编写更好的C#代码
编写更好的C#代码作为全栈工程师我每天都在与C#打交道。从最初的“能跑就行”到现在的“优雅、高效、可维护”这条进化之路充满了教训。今天我想从实战角度分享那些真正能提升你C#代码质量的技巧——不是教科书上的理论而是我在生产环境中踩过坑后总结出的经验。### 1. 拥抱不可变性让Bug无处藏身在分布式系统和多线程环境中可变状态是万恶之源。C#中我们常常习惯性地写var list new Listint()然后到处Add、Remove。但更好的做法是尽可能使用不可变集合或只读接口。实战案例在一个订单处理系统中我遇到过因为某个服务意外修改了共享的配置列表导致其他请求读到脏数据的问题。改用IReadOnlyListT后这种问题彻底消失。csharp// 反面教材暴露了可变的List调用方可以随意修改public class OrderService_Bad{ public Liststring StatusHistory { get; set; } new Liststring(); public void UpdateStatus(string status) { StatusHistory.Add(status); // 内部修改 // 外部也可以直接 StatusHistory.Clear()破坏性极大 }}// 正面教材只暴露只读接口内部用不可变逻辑public class OrderService_Good{ private readonly Liststring _statusHistory new Liststring(); // 只读视图外部无法修改 public IReadOnlyListstring StatusHistory _statusHistory.AsReadOnly(); public void UpdateStatus(string status) { // 内部仍然可以修改但外部无法干涉 _statusHistory.Add(status); }}关键点- 属性尽量使用IReadOnlyListT、IReadOnlyDictionaryK,V等只读接口- 对于值类型考虑使用readonly struct- 字段尽量声明为readonly### 2. 用模式匹配代替冗长的if-else链C# 9的模式匹配极大简化了条件逻辑。我经常在支付网关集成中处理不同支付方式的响应传统的写法又长又容易出错。csharp// 传统写法一堆if-else难以阅读和扩展public string ProcessPayment_Old(object payment){ if (payment is CreditCardPayment cc) { if (cc.IsValid cc.Amount 0) return $信用卡支付 {cc.Amount:C} 成功; else return 信用卡支付失败; } else if (payment is PayPalPayment pp) { if (!string.IsNullOrEmpty(pp.Email) pp.Amount 0) return $PayPal支付 {pp.Amount:C} 成功; else return PayPal支付失败; } // 每增加一种支付方式就要加一个else if return 未知支付方式;}// 模式匹配写法优雅、类型安全、易扩展public string ProcessPayment_New(object payment){ return payment switch { // 属性模式 条件过滤 CreditCardPayment { IsValid: true, Amount: 0 } cc $信用卡支付 {cc.Amount:C} 成功, PayPalPayment { Email: not null or , Amount: 0 } pp $PayPal支付 {pp.Amount:C} 成功, _ 未知或无效的支付方式 };}// 辅助类定义public record CreditCardPayment(decimal Amount, bool IsValid);public record PayPalPayment(decimal Amount, string Email);实战心得- 使用switch表达式替代复杂的if-else链代码可读性提升50%以上- 结合属性模式{ Property: Condition }和关系模式,一行顶十行- 新增支付方式时只需加一个分支不改动其他逻辑### 3. 异步编程不要阻塞要流式在I/O密集型的Web应用中正确使用async/await是性能的关键。我见过太多代码把异步方法用.Result或.Wait()阻塞调用最后导致线程池饥饿、性能雪崩。csharp// 反模式同步阻塞异步调用死锁温床public string GetUserData_Bad(int userId){ var httpClient new HttpClient(); var response httpClient.GetAsync($https://api.example.com/users/{userId}).Result; // 危险 return response.Content.ReadAsStringAsync().Result; // 更危险}// 正确做法全链路异步public async Taskstring GetUserData_Good(int userId){ using var httpClient new HttpClient(); // 记得释放资源 var response await httpClient.GetAsync($https://api.example.com/users/{userId}); return await response.Content.ReadAsStringAsync();}// 实际调用场景比如在Controller中public async TaskIActionResult ShowUser(int id){ var data await GetUserData_Good(id); // 一路异步不阻塞 return Ok(data);}进阶技巧- 使用ValueTask减少高频率异步调用时的内存分配- 用Task.WhenAll并行处理多个独立异步任务而不是顺序await- 在库代码中不要用async void除非是事件处理器csharp// 高效并行的例子public async Taskstring[] FetchMultipleUrlsAsync(IEnumerablestring urls){ using var client new HttpClient(); var tasks urls.Select(url client.GetStringAsync(url)); return await Task.WhenAll(tasks); // 全部并行执行}### 4. 利用LINQ提升数据处理的表达力LINQ不仅仅是语法糖它改变了我们处理集合的思维方式。在数据分析、报表生成等场景LINQ让代码既简洁又高效。csharp// 场景从一堆订单中找出金额最高的VIP客户计算其平均消费var orders new ListOrder{ new Order { CustomerId 1, Amount 100, IsVip true }, new Order { CustomerId 2, Amount 300, IsVip false }, new Order { CustomerId 1, Amount 500, IsVip true }, new Order { CustomerId 3, Amount 200, IsVip true }, new Order { CustomerId 1, Amount 150, IsVip true },};// 传统写法嵌套循环冗长var vipOrders new ListOrder();foreach (var o in orders){ if (o.IsVip) vipOrders.Add(o);}// 然后还要按客户分组、算平均、找最大... 代码会非常啰嗦// LINQ写法声明式一目了然var result orders .Where(o o.IsVip) // 过滤VIP订单 .GroupBy(o o.CustomerId) // 按客户分组 .Select(g new { CustomerId g.Key, AvgAmount g.Average(o o.Amount), // 平均消费 TotalAmount g.Sum(o o.Amount) // 总消费 }) .OrderByDescending(x x.TotalAmount) // 按总消费降序 .First(); // 取第一个最高的Console.WriteLine($VIP客户{result.CustomerId} 平均消费{result.AvgAmount:C}总消费{result.TotalAmount:C});// 辅助类public class Order{ public int CustomerId { get; set; } public decimal Amount { get; set; } public bool IsVip { get; set; }}实战建议- 优先使用LINQ方法链但注意不要过度使用导致性能问题比如在循环中重复计算- 对于大数据集考虑AsParallel()但要注意线程安全- 使用Select投影时尽量只取需要的字段减少内存占用### 5. 防御式编程让错误无处遁形好的C#代码不仅要处理正常流程还要优雅地处理异常。我习惯用“快速失败”原则但也要提供有意义的错误信息。csharp// 防御式编程示例public async TaskCustomer GetCustomerAsync(int customerId){ // 参数验证快速失败 ArgumentNullException.ThrowIfNull(customerId); if (customerId 0) { throw new ArgumentOutOfRangeException(nameof(customerId), 客户ID必须为正数); } try { // 模拟数据库查询 var customer await _dbContext.Customers.FindAsync(customerId); // 结果验证 return customer ?? throw new NotFoundException($客户 {customerId} 不存在); } catch (NotFoundException) { // 特定异常让上层处理 throw; } catch (Exception ex) { // 记录日志抛出有意义的异常 _logger.LogError(ex, 查询客户 {CustomerId} 时发生错误, customerId); throw new ApplicationException(数据库操作失败请稍后重试, ex); }}最佳实践- 使用ArgumentNullException.ThrowIfNull().NET 7简化参数检查- 自定义异常类不要直接抛Exception- 捕获异常时要么处理并恢复要么重新抛出保留堆栈信息### 6. 性能优化从细节做起在大规模系统中微小的性能问题会放大。这里分享几个我常用的性能优化技巧csharp// 1. 使用StringBuilder处理字符串拼接// 反模式在循环中拼接字符串var sb new StringBuilder();foreach (var item in items){ sb.Append(item.Name).Append(;);}var result sb.ToString();// 2. 避免不必要的装箱拆箱int number 42;// 反模式隐式装箱object boxed number;// 好做法如果必须用object用泛型// 3. 使用ArrayPool处理大数组byte[] buffer ArrayPoolbyte.Shared.Rent(1024 * 1024);try{ // 使用buffer}finally{ ArrayPoolbyte.Shared.Return(buffer);}更实用的技巧- 在热路径中避免LINQ改用普通循环尽管LINQ更优雅但有额外开销- 使用SpanT处理内存操作避免复制- 对于频繁调用的方法考虑缓存结果### 总结编写更好的C#代码远不止是语法层面的技巧。从实战角度出发我总结出以下核心原则1.不可变性优先减少状态共享让并发更安全2.表达力优先用模式匹配、LINQ等现代特性让代码自文档化3.异步全链路阻塞是性能杀手坚持async/await4.防御式编程验证输入、明确异常、记录日志5.性能意识理解底层机制优化关键路径但这些原则不是孤立的。真正的“更好”是平衡——在可读性、性能、可维护性和开发效率之间找到最佳点。比如不要为了用LINQ而用LINQ如果普通循环更清晰就用循环不要为了不可变性而过度设计如果项目是单线程且简单直接写也没问题。最后记住代码是写给人看的只是顺便让机器执行。写出让同事和三个月后的自己能轻松理解的代码才是真正的“更好”。希望这些实战经验能帮你在C#开发之路上走得更远。