编写更好的C#代码
作为全栈工程师,我每天都在与C#打交道。从最初的“能跑就行”到现在的“优雅、高效、可维护”,这条进化之路充满了教训。今天,我想从实战角度,分享那些真正能提升你C#代码质量的技巧——不是教科书上的理论,而是我在生产环境中踩过坑后总结出的经验。### 1. 拥抱不可变性:让Bug无处藏身在分布式系统和多线程环境中,可变状态是万恶之源。C#中,我们常常习惯性地写var list = new List<int>(),然后到处Add、Remove。但更好的做法是,尽可能使用不可变集合或只读接口。实战案例:在一个订单处理系统中,我遇到过因为某个服务意外修改了共享的配置列表,导致其他请求读到脏数据的问题。改用IReadOnlyList<T>后,这种问题彻底消失。csharp// 反面教材:暴露了可变的List,调用方可以随意修改public class OrderService_Bad{ public List<string> StatusHistory { get; set; } = new List<string>(); public void UpdateStatus(string status) { StatusHistory.Add(status); // 内部修改 // 外部也可以直接 StatusHistory.Clear(),破坏性极大 }}// 正面教材:只暴露只读接口,内部用不可变逻辑public class OrderService_Good{ private readonly List<string> _statusHistory = new List<string>(); // 只读视图,外部无法修改 public IReadOnlyList<string> StatusHistory => _statusHistory.AsReadOnly(); public void UpdateStatus(string status) { // 内部仍然可以修改,但外部无法干涉 _statusHistory.Add(status); }}关键点:- 属性尽量使用IReadOnlyList<T>、IReadOnlyDictionary<K,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 Task<string> 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 Task<IActionResult> ShowUser(int id){ var data = await GetUserData_Good(id); // 一路异步,不阻塞 return Ok(data);}进阶技巧:- 使用ValueTask减少高频率异步调用时的内存分配- 用Task.WhenAll并行处理多个独立异步任务,而不是顺序await- 在库代码中不要用async void,除非是事件处理器csharp// 高效并行的例子public async Task<string[]> FetchMultipleUrlsAsync(IEnumerable<string> 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 List<Order>{ 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 List<Order>();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 Task<Customer> 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 = ArrayPool<byte>.Shared.Rent(1024 * 1024);try{ // 使用buffer}finally{ ArrayPool<byte>.Shared.Return(buffer);}更实用的技巧:- 在热路径中避免LINQ,改用普通循环(尽管LINQ更优雅,但有额外开销)- 使用Span<T>处理内存操作,避免复制- 对于频繁调用的方法,考虑缓存结果### 总结编写更好的C#代码,远不止是语法层面的技巧。从实战角度出发,我总结出以下核心原则:1.不可变性优先:减少状态共享,让并发更安全2.表达力优先:用模式匹配、LINQ等现代特性,让代码自文档化3.异步全链路:阻塞是性能杀手,坚持async/await4.防御式编程:验证输入、明确异常、记录日志5.性能意识:理解底层机制,优化关键路径但这些原则不是孤立的。真正的“更好”是平衡——在可读性、性能、可维护性和开发效率之间找到最佳点。比如,不要为了用LINQ而用LINQ,如果普通循环更清晰就用循环;不要为了不可变性而过度设计,如果项目是单线程且简单,直接写也没问题。最后,记住:代码是写给人看的,只是顺便让机器执行。写出让同事(和三个月后的自己)能轻松理解的代码,才是真正的“更好”。希望这些实战经验能帮你在C#开发之路上走得更远。