.NET编码规范04-代码布局与注释规范

.NET编码规范

第 4 篇:代码布局与注释规范

—— 可读性即正义

前言

代码的"颜值"直接影响开发效率和维护成本。格式凌乱的代码就像一篇没有段落的文章,读起来令人窒息;而格式一致的代码则如排版精良的书籍,阅读者可以专注于内容本身。

本文深入讲解 C# 的代码布局、格式规范与注释编写技巧。


一、大括号风格:Allman Style

C# 官方和 .NET 社区采用Allman 风格(也称为 “BSD style”)—— 大括号独占一行。

// ✅ Allman Style —— C# 社区标准publicvoidProcessOrder(Orderorder){if(order.IsValid){foreach(variteminorder.Items){UpdateInventory(item);}}}// ❌ K&R Style(Java/JavaSript 常用)—— 不符合 C# 规范publicvoidProcessOrder(Orderorder){if(order.IsValid){foreach(variteminorder.Items){UpdateInventory(item);}}}

为什么 C# 选 Allman?

  • 大括号对齐让代码块边界一目了然
  • Visual Studio 默认就是 Allman 风格
  • .NET 运行时源码统一使用此风格

二、空行与间距的艺术

2.1 成员间的空行

publicclassCustomer{// 属性与属性之间:0-1 个空行publicstringFirstName{get;set;}publicstringLastName{get;set;}// 属性组与方法之间:至少 1 个空行publicstringFullName=>$"{FirstName}{LastName}";publicvoidUpdateProfile(ProfileDtodto){// ...}// 方法与方法之间:至少 1 个空行publicasyncTaskSaveAsync(){// ...}}

2.2 逻辑块内空行

publicasyncTask<OrderResult>PlaceOrderAsync(OrderRequestrequest){// 验证参数ArgumentNullException.ThrowIfNull(request);if(!request.Items.Any())thrownewValidationException("Order must have at least one item.");// 准备订单varorder=newOrder{CustomerId=request.CustomerId,CreatedAt=DateTime.UtcNow,Items=request.Items.Select(i=>newOrderItem(i)).ToList()};// 计算总价order.TotalAmount=order.Items.Sum(i=>i.UnitPrice*i.Quantity);// 持久化await_orderRepository.AddAsync(order);await_unitOfWork.SaveChangesAsync();return_mapper.Map<OrderResult>(order);}

用空行将方法内的不同逻辑块分隔开,让代码像段落一样可读。

2.3 代码行长度

// ✅ 长语句换行,运算符前换行(.NET 推荐风格)varsummary=newStringBuilder().AppendLine("=== Report Summary ===").AppendFormat("Total Orders: {0}",totalOrders).AppendFormat("Total Revenue: {0:C}",totalRevenue).AppendLine().ToString();// ✅ 方法调用参数过多时换行varcustomer=await_customerService.GetCustomerWithOrdersAsync(customerId:request.CustomerId,includeDetails:true,dateRange:newDateRange(startDate,endDate),cancellationToken:ct);// ✅ 长 LINQ 查询换行varactiveCustomers=await_dbContext.Customers.Where(c=>c.Status==CustomerStatus.Active).Where(c=>c.CreatedAt>=thirtyDaysAgo).OrderByDescending(c=>c.LastLoginAt).Take(100).ToListAsync(ct);

2.4 空格使用规则

// 关键字与括号之间:一个空格if(condition){}while(condition){}for(inti=0;i<count;i++){}// 运算符两侧:一个空格intresult=a+b*c;boolisValid=(value1>0)&&(value2<100);// 方法参数逗号后:一个空格Calculate(price,quantity,discount);

三、var关键字使用准则

var不是语法糖那么简单,用得好让代码更简洁,用不好则让代码晦涩难懂。

3.1 应该使用var的场景

// ✅ 类型从右侧明显可见(new 表达式、字面量、显式转换)varcustomer=newCustomer();varitems=newList<string>();varprice=29.99m;varname="John";varresult=(Customer)obj;// ✅ 创建复杂泛型类型vardictionary=newDictionary<string,List<Order>>();// ✅ LINQ 查询结果varactiveUsers=users.Where(u=>u.IsActive).ToList();// ✅ for 循环变量for(vari=0;i<items.Count;i++){}// ✅ 使用 target-typed new(C# 9+)List<string>names=new();Customercustomer=new(){Name="John"};

3.2 不应该使用var的场景

// ❌ 类型从右侧不明显varresult=GetSomething();// 返回什么类型?vardata=ProcessData(input);// 模糊varvalue=Calculate();// 更模糊// ✅ 应使用显式类型IEnumerable<Order>result=GetSomething();ReportDatadata=ProcessData(input);decimalvalue=Calculate();// ❌ foreach 中不建议用 varforeach(varitemincollection){}// item 是什么类型?// ✅ 使用显式类型foreach(stringnameinnames){}foreach(Customercustomerincustomers){}

3.3 一条判断法则

如果你需要把鼠标悬停到var上才能知道类型,那就别用var


四、表达式与语句的选择

4.1 expression-bodied members 的适用场景

// ✅ 适合简单属性publicstringFullName=>$"{FirstName}{LastName}";publicboolHasOrders=>Orders.Count>0;// ✅ 适合简单方法publicoverridestringToString()=>$"{Id}:{Name}";publicstaticCustomerEmpty=>new();// ❌ 不适合复杂逻辑publicdecimalGetTotalPrice()=>Items.Where(i=>i.IsActive).Select(i=>i.Price*i.Quantity).Sum(i=>ApplyDiscount(i,GetDiscountRate()));// 太长

原则:一行能写完的单表达式适合=>,多行逻辑保持{ }

4.2 switch 表达式 vs switch 语句

// ✅ 简单值映射 → switch 表达式stringstatusText=order.Statusswitch{OrderStatus.Pending=>"待处理",OrderStatus.Processing=>"处理中",OrderStatus.Completed=>"已完成",OrderStatus.Cancelled=>"已取消",_=>"未知状态"};// ✅ 复杂逻辑 → switch 语句switch(order.Status){caseOrderStatus.Pending:ValidateStock(order);ReserveInventory(order);break;caseOrderStatus.Processing:// 复杂业务逻辑...break;}

五、注释规范

5.1 XML 文档注释(///Summary)

所有public成员都应该有 XML 文档注释:

/// <summary>/// 根据用户ID获取用户信息,包含关联的订单数据。/// </summary>/// <param name="userId">用户唯一标识符。</param>/// <param name="includeOrders">是否包含关联订单。默认为 false。</param>/// <param name="cancellationToken">取消令牌。</param>/// <returns>包含用户信息的 <see cref="UserDto"/> 对象,若用户不存在则返回 null。</returns>/// <exception cref="ArgumentException">当 userId 为空时抛出。</exception>/// <exception cref="OperationCanceledException">当操作被取消时抛出。</exception>publicasyncTask<UserDto?>GetUserByIdAsync(stringuserId,boolincludeOrders=false,CancellationTokencancellationToken=default){// ...}

XML 注释写作要点:

  • 以大写字母开始文本,以句号结束
  • 每个paramreturnsexception都要写明
  • returns中说明 null 返回情况
  • 使用<see cref=""/>引用其他类型
  • 使用<c></c>标记代码片段

5.2 单行注释(//

// ✅ 注释在代码上方单独一行// 计算应用折扣后的最终价格decimalfinalPrice=CalculateDiscountedPrice(originalPrice,discountRate);// ❌ 不要放在代码行末尾decimalfinalPrice=CalculateDiscountedPrice(originalPrice,discountRate);// 计算价格// ✅ `// ` —— 分隔符后加一个空格// ✅ 以大写字母开头,以句号结束。

5.3 注释的"为什么"而非"是什么"

// ❌ 坏注释 —— 解释"是什么"(代码本身已经表达了)// 将 count 加 1count++;// ✅ 好注释 —— 解释"为什么"(代码无法表达)// 加 1 补偿 0-based 索引,使结果与用户可见的 1-based 序号一致count++;// ❌ 坏注释 —— 显而易见的代码不需要注释// 创建新的 Order 对象varorder=newOrder();// ✅ 好注释 —— 解释业务含义// 订单金额超过 ¥5000 时,按 VIP 折扣重新计算(营销规则 MKT-2024-03)if(order.TotalAmount>5000m){order.TotalAmount=ApplyVipDiscount(order);}

5.4 特殊标记注释

// TODO(w4980): 2024-06-15 - 待采购接口对接后启用此段逻辑// HACK(w4980): 2024-06-15 - 临时绕过三方 API 的编码问题,API v3 修复后移除// FIXME(w4980): 2024-06-15 - 并发场景下存在竞态条件,需用 ConcurrentDictionary// NOTE(w4980): 2024-06-15 - O(n²) 复杂度,数据量超过 10K 时需优化

5.5 被注释掉的代码

// ❌ 不要保留大段注释代码// public void OldMethod()// {// // ... 50 行被注释的代码 ...// }// ✅ 如果只是临时保留,附上说明// --- 旧版积分计算逻辑(2024-06-15,新逻辑在 PointsCalculatorV2)// 确认新逻辑生效 1 个月后,删除以下代码// public int CalculatePoints_Old()// {// ...// }

原则:代码仓库有 Git 历史,删掉的代码随时可以找回。注释掉的代码只是噪音。


六、#region的使用

// ❌ 过度使用 region —— 隐藏代码结构#regionPropertiespublicstringName{get;set;}publicintAge{get;set;}#endregion#regionMethodspublicvoidDoThis(){}publicvoidDoThat(){}#endregion// ✅ 仅在确实需要折叠"样板代码"时使用// 例如:大量重复的接口实现#regionIDisposable ImplementationpublicvoidDispose(){...}protectedvirtualvoidDispose(booldisposing){...}#endregion

七、实战清单

日常开发自查项

  • 大括号是否独立成行(Allman Style)?
  • 成员之间是否有适当的空行?
  • var使用是否得当(类型明显时用,不明显时不用)?
  • 所有public成员是否有 XML 文档注释?
  • 注释是否解释了"为什么"而非"是什么"?
  • 是否有被注释掉的大段代码?
  • 是否有TODO/HACK标记注明人和时间?
  • 长语句是否合理换行?