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

📅 2026/7/9 7:25:29
.NET编码规范04-代码布局与注释规范
.NET编码规范第 4 篇代码布局与注释规范—— 可读性即正义前言代码的颜值直接影响开发效率和维护成本。格式凌乱的代码就像一篇没有段落的文章读起来令人窒息而格式一致的代码则如排版精良的书籍阅读者可以专注于内容本身。本文深入讲解 C# 的代码布局、格式规范与注释编写技巧。一、大括号风格Allman StyleC# 官方和 .NET 社区采用Allman 风格也称为 “BSD style”—— 大括号独占一行。// ✅ Allman Style —— C# 社区标准publicvoidProcessOrder(Orderorder){if(order.IsValid){foreach(variteminorder.Items){UpdateInventory(item);}}}// ❌ KR StyleJava/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 逻辑块内空行publicasyncTaskOrderResultPlaceOrderAsync(OrderRequestrequest){// 验证参数ArgumentNullException.ThrowIfNull(request);if(!request.Items.Any())thrownewValidationException(Order must have at least one item.);// 准备订单varordernewOrder{CustomerIdrequest.CustomerId,CreatedAtDateTime.UtcNow,Itemsrequest.Items.Select(inewOrderItem(i)).ToList()};// 计算总价order.TotalAmountorder.Items.Sum(ii.UnitPrice*i.Quantity);// 持久化await_orderRepository.AddAsync(order);await_unitOfWork.SaveChangesAsync();return_mapper.MapOrderResult(order);}用空行将方法内的不同逻辑块分隔开让代码像段落一样可读。2.3 代码行长度// ✅ 长语句换行运算符前换行.NET 推荐风格varsummarynewStringBuilder().AppendLine( Report Summary ).AppendFormat(Total Orders: {0},totalOrders).AppendFormat(Total Revenue: {0:C},totalRevenue).AppendLine().ToString();// ✅ 方法调用参数过多时换行varcustomerawait_customerService.GetCustomerWithOrdersAsync(customerId:request.CustomerId,includeDetails:true,dateRange:newDateRange(startDate,endDate),cancellationToken:ct);// ✅ 长 LINQ 查询换行varactiveCustomersawait_dbContext.Customers.Where(cc.StatusCustomerStatus.Active).Where(cc.CreatedAtthirtyDaysAgo).OrderByDescending(cc.LastLoginAt).Take(100).ToListAsync(ct);2.4 空格使用规则// 关键字与括号之间一个空格if(condition){}while(condition){}for(inti0;icount;i){}// 运算符两侧一个空格intresultab*c;boolisValid(value10)(value2100);// 方法参数逗号后一个空格Calculate(price,quantity,discount);三、var关键字使用准则var不是语法糖那么简单用得好让代码更简洁用不好则让代码晦涩难懂。3.1 应该使用var的场景// ✅ 类型从右侧明显可见new 表达式、字面量、显式转换varcustomernewCustomer();varitemsnewListstring();varprice29.99m;varnameJohn;varresult(Customer)obj;// ✅ 创建复杂泛型类型vardictionarynewDictionarystring,ListOrder();// ✅ LINQ 查询结果varactiveUsersusers.Where(uu.IsActive).ToList();// ✅ for 循环变量for(vari0;iitems.Count;i){}// ✅ 使用 target-typed newC# 9Liststringnamesnew();Customercustomernew(){NameJohn};3.2 不应该使用var的场景// ❌ 类型从右侧不明显varresultGetSomething();// 返回什么类型vardataProcessData(input);// 模糊varvalueCalculate();// 更模糊// ✅ 应使用显式类型IEnumerableOrderresultGetSomething();ReportDatadataProcessData(input);decimalvalueCalculate();// ❌ foreach 中不建议用 varforeach(varitemincollection){}// item 是什么类型// ✅ 使用显式类型foreach(stringnameinnames){}foreach(Customercustomerincustomers){}3.3 一条判断法则如果你需要把鼠标悬停到var上才能知道类型那就别用var。四、表达式与语句的选择4.1 expression-bodied members 的适用场景// ✅ 适合简单属性publicstringFullName${FirstName}{LastName};publicboolHasOrdersOrders.Count0;// ✅ 适合简单方法publicoverridestringToString()${Id}:{Name};publicstaticCustomerEmptynew();// ❌ 不适合复杂逻辑publicdecimalGetTotalPrice()Items.Where(ii.IsActive).Select(ii.Price*i.Quantity).Sum(iApplyDiscount(i,GetDiscountRate()));// 太长原则一行能写完的单表达式适合多行逻辑保持{ }。4.2 switch 表达式 vs switch 语句// ✅ 简单值映射 → switch 表达式stringstatusTextorder.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 nameuserId用户唯一标识符。/param/// param nameincludeOrders是否包含关联订单。默认为 false。/param/// param namecancellationToken取消令牌。/param/// returns包含用户信息的 see crefUserDto/ 对象若用户不存在则返回 null。/returns/// exception crefArgumentException当 userId 为空时抛出。/exception/// exception crefOperationCanceledException当操作被取消时抛出。/exceptionpublicasyncTaskUserDto?GetUserByIdAsync(stringuserId,boolincludeOrdersfalse,CancellationTokencancellationTokendefault){// ...}XML 注释写作要点以大写字母开始文本以句号结束每个param、returns、exception都要写明returns中说明 null 返回情况使用see cref/引用其他类型使用c/c标记代码片段5.2 单行注释//// ✅ 注释在代码上方单独一行// 计算应用折扣后的最终价格decimalfinalPriceCalculateDiscountedPrice(originalPrice,discountRate);// ❌ 不要放在代码行末尾decimalfinalPriceCalculateDiscountedPrice(originalPrice,discountRate);// 计算价格// ✅ // —— 分隔符后加一个空格// ✅ 以大写字母开头以句号结束。5.3 注释的为什么而非是什么// ❌ 坏注释 —— 解释是什么代码本身已经表达了// 将 count 加 1count;// ✅ 好注释 —— 解释为什么代码无法表达// 加 1 补偿 0-based 索引使结果与用户可见的 1-based 序号一致count;// ❌ 坏注释 —— 显而易见的代码不需要注释// 创建新的 Order 对象varordernewOrder();// ✅ 好注释 —— 解释业务含义// 订单金额超过 ¥5000 时按 VIP 折扣重新计算营销规则 MKT-2024-03if(order.TotalAmount5000m){order.TotalAmountApplyVipDiscount(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标记注明人和时间长语句是否合理换行