关于 modern-csharp-coding-standards
面向编写和重构 C# 代码的技能,涵盖 API、领域模型、异步代码与性能场景。重点提供现代 C# 的类型设计、代码风格和实践规范。
主要功能
- 01默认采用不可变类型
- 02空值安全与类型安全
- 03异步与取消令牌规范
- 04Span 和 Memory 性能实践
- 05API 设计与命名规范
适用场景
- 01新建或重构 C# 代码
- 02设计库或服务的公共 API
- 03优化高性能代码路径
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill modern-csharp-coding-standards安装后在 Claude Code 中输入/modern-csharp-coding-standards确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
reference/anti-patterns.mdapi-design.mderror-handling.mdlanguage-patterns.mdperformance-patterns.md详细介绍
现代 C# 编码规范
何时使用此技能
在以下情况下使用此技能:
- 编写新的 C# 代码或重构现有代码
- 为库或服务设计公共 API
- 优化性能关键代码路径
- 使用强类型实现领域模型
- 构建大量使用 async/await 的应用程序
- 处理二进制数据、缓冲区或高吞吐量场景
核心原则
- 默认不可变 - 使用
record类型和仅限init的属性 - 类型安全 - 利用可空引用类型和值对象
- 现代模式匹配 - 广泛使用
switch表达式和模式 - 全面异步 - 优先使用支持适当取消机制的异步 API
- 零分配模式 - 在性能关键代码中使用
Span<T>和Memory<T> - API 设计 - 接受抽象类型,返回适当具体的类型
- 组合优于继承 - 避免抽象基类,优先使用组合
- 将值对象定义为结构体 - 对值对象使用
readonly record struct
命名约定
一般规则
| 元素 | 约定 | 示例 |
|---|---|---|
| 命名空间 | PascalCase,以点分隔 | MyCompany.MyProduct.Core |
| 类、记录、结构体 | PascalCase | OrderService、OrderSummary |
| 接口 | I + PascalCase | IOrderRepository |
| 方法 | PascalCase | GetOrderAsync |
| 属性 | PascalCase | OrderDate |
| 事件 | PascalCase | OrderCompleted |
| 公共常量 | PascalCase | MaxRetryCount |
| 私有字段 | _camelCase | _orderRepository |
| 参数、局部变量 | camelCase | orderId、totalAmount |
| 类型参数 | T 或 T + PascalCase | T、TKey、TValue |
| 枚举成员 | PascalCase | OrderStatus.Pending |
异步方法命名
异步方法以 Async 作为后缀:
public Task<Order> GetOrderAsync(int id);
public ValueTask SaveChangesAsync(CancellationToken ct);
Exception: Event handlers and interface implementations where the framework does not use the `Async` suffix (e.g., ASP.NET Core middleware `InvokeAsync` is already named by the framework).
布尔值命名
布尔值以 is、has、can、should 或类似前缀开头:
public bool IsActive { get; set; }
public bool HasOrders { get; }
public bool CanDelete(Order order);
集合命名
集合使用复数名词:
public IReadOnlyList<Order> Orders { get; }
public Dictionary<string, int> CountsByName { get; }
文件组织
每个文件一个类型
每个顶级类型(类、记录、结构体、接口、枚举)都应放在单独的文件中,文件名与类型名完全相同。嵌套类型保留在其所属类型的文件中。
OrderService.cs -> public class OrderService
IOrderRepository.cs -> public interface IOrderRepository
OrderStatus.cs -> public enum OrderStatus
OrderSummary.cs -> public record OrderSummary
文件范围命名空间
始终使用文件范围命名空间(C# 10+):
namespace MyApp.Services;
public class OrderService { }
using 指令
将 using 指令放在文件顶部、命名空间外部。启用 <ImplicitUsings>enable</ImplicitUsings>(现代 .NET 中的默认设置)后,常用命名空间已自动导入。
using 指令的顺序:
System.*命名空间- 第三方命名空间
- 项目命名空间
代码风格
大括号
控制流始终使用大括号,即使代码块只有一行:
if (order.IsValid)
{
Process(order);
}
表达式体成员
单表达式成员使用表达式体:
public string FullName => $"{FirstName} {LastName}";
public override string ToString() => $"Order #{Id}";
var 的使用
当类型可从右侧表达式明显推断时,使用 var:
var orders = new List<Order>();
var customer = GetCustomerById(id);
IOrderRepository repo = serviceProvider.GetRequiredService<IOrderRepository>();
decimal total = CalculateTotal(items);
空值处理
优先使用模式匹配,而非空值检查:
if (order is not null) { }
if (order is { Status: OrderStatus.Active }) { }
var name = customer?.Name ?? "Unknown";
var orders = customer?.Orders ?? [];
items ??= [];
字符串处理
优先使用字符串插值,而非字符串拼接或 string.Format:
var message = $"Order {orderId} totals {total:C2}";
var json = $$"""
{
"id": {{orderId}},
"name": "{{name}}"
}
""";
访问修饰符
始终显式指定访问修饰符,不要依赖默认值:
public class OrderService
{
private readonly IOrderRepository _repo;
internal void ProcessBatch() { }
}
修饰符顺序
access (public/private/protected/internal) -> static -> extern -> new ->
virtual/abstract/override/sealed -> readonly -> volatile -> async -> partial
public static readonly int MaxSize = 100;
protected virtual async Task<Order> LoadAsync() => await repo.GetDefaultAsync();
public sealed override string ToString() => Name;
类型设计
默认密封类
对非继承设计的类使用密封。这可以提升性能(去虚拟化),并明确表达设计意图:
public sealed class OrderService(IOrderRepository repo)
{
}
只有在明确将类设计为基类时,才不使用密封。
优先使用组合,而非继承
public sealed class OrderProcessor(IValidator validator, INotifier notifier)
{
public async Task ProcessAsync(Order order)
{
await validator.ValidateAsync(order);
await notifier.NotifyAsync(order);
}
}
接口隔离
保持接口专注。优先使用多个小型接口,而非一个大型接口:
public interface IOrderReader
{
Task<Order?> GetByIdAsync(int id, CancellationToken ct = default);
Task<IReadOnlyList<Order>> GetAllAsync(CancellationToken ct = default);
}
public interface IOrderWriter
{
Task<Order> CreateAsync(Order order, CancellationToken ct = default);
Task UpdateAsync(Order order, CancellationToken ct = default);
}
语言模式
有关详细指南,请参阅语言模式:
- 不可变数据使用记录(C# 9+)
- 值对象使用 readonly record struct
- 模式匹配(C# 8-12)
- 可空引用类型(C# 8+)
- 组合优于继承
性能模式
详细指南请参阅性能模式,内容包括:
- Async/Await 最佳实践
- 使用 Span 和 Memory 编写零分配代码
API 设计原则
详细指南请参阅API 设计原则,内容包括:
- 接受抽象类型,并返回适当具体的类型
- 方法签名最佳实践
错误处理
详细指南请参阅错误处理,内容包括:
- Result 类型模式(铁路导向编程)
测试模式
public record OrderBuilder
{
public OrderId Id { get; init; } = OrderId.New();
public CustomerId CustomerId { get; init; } = CustomerId.New();
public Money Total { get; init; } = new Money(100m, "USD");
public IReadOnlyList<OrderItem> Items { get; init; } = Array.Empty<OrderItem>();
public Order Build() => new(Id, CustomerId, Total, Items);
}
[Fact]
public void CalculateDiscount_LargeOrder_AppliesCorrectDiscount()
{
var baseOrder = new OrderBuilder().Build();
var largeOrder = baseOrder with { Total = new Money(1500m, "USD") };
var discount = _service.CalculateDiscount(largeOrder);
discount.Should().Be(new Money(225m, "USD"));
}
[Theory]
[InlineData("ORD-12345", true)]
[InlineData("INVALID", false)]
public void TryParseOrderId_VariousInputs_ReturnsExpectedResult(
string input, bool expected)
{
var result = OrderIdParser.TryParse(input.AsSpan(), out var orderId);
result.Should().Be(expected);
}
[Fact]
public void Money_Add_SameCurrency_ReturnsSum()
{
var money1 = new Money(100m, "USD");
var money2 = new Money(50m, "USD");
var result = money1.Add(money2);
result.Should().Be(new Money(150m, "USD"));
}
[Fact]
public void Money_Add_DifferentCurrency_ThrowsException()
{
var usd = new Money(100m, "USD");
var eur = new Money(50m, "EUR");
var act = () => usd.Add(eur);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*different currencies*");
}
CancellationToken 约定
在异步方法中,将 CancellationToken 作为最后一个参数。可选令牌的默认值使用 default:
public async Task<Order> GetOrderAsync(int id, CancellationToken ct = default)
{
return await _repo.GetByIdAsync(id, ct);
}
始终将令牌传递给下游异步调用。绝不要忽略收到的 CancellationToken。
XML 文档
为公共 API 添加 XML 文档。内容应简洁:
/// <summary>
/// Retrieves an order by its unique identifier.
/// </summary>
/// <param name="id">The order identifier.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The order, or <see langword="null"/> if not found.</returns>
public Task<Order?> GetByIdAsync(int id, CancellationToken ct = default);
以下情况无需添加 XML 文档:
- 私有或内部成员(除非是库的
InternalsVisibleToAPI) - 一目了然的成员(例如,
public string Name { get; }) - 测试方法
避免基于反射的元编程
详细指南请参阅反模式,内容包括:
- 避免使用 AutoMapper、Mapster 及类似的基于反射的库的原因
- 改用显式映射方法
- 在确有反射需求时使用 UnsafeAccessorAttribute
应避免的反模式
详细指南请参阅反模式,内容包括:
- 可变 DTO
- 用类表示值对象
- 深层继承层次结构
- 暴露可变集合
- 忘记 CancellationToken
- 阻塞异步代码
代码组织
namespace MyApp.Domain.Orders;
public record Order(
OrderId Id,
CustomerId CustomerId,
Money Total,
OrderStatus Status,
IReadOnlyList<OrderItem> Items
)
{
public bool IsCompleted => Status is OrderStatus.Completed;
public Result<Order, OrderError> AddItem(OrderItem item)
{
if (Status is not OrderStatus.Draft)
return Result<Order, OrderError>.Failure(
new OrderError("ORDER_NOT_DRAFT", "Can only add items to draft orders"));
var newItems = Items.Append(item).ToList();
var newTotal = new Money(
Items.Sum(i => i.Total.Amount) + item.Total.Amount,
Total.Currency);
return Result<Order, OrderError>.Success(
this with { Items = newItems, Total = newTotal });
}
}
public enum OrderStatus
{
Draft,
Submitted,
Processing,
Completed,
Cancelled
}
public record OrderItem(
ProductId ProductId,
Quantity Quantity,
Money UnitPrice
)
{
public Money Total => new(
UnitPrice.Amount * Quantity.Value,
UnitPrice.Currency);
}
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
}
public readonly record struct OrderError(string Code, string Message);
分析器强制执行
在 Directory.Build.props 或 .editorconfig 中配置以下分析器,以自动执行规范:
<PropertyGroup>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-all</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
C# 风格的关键 .editorconfig 规则:
[*.cs]
csharp_style_namespace_declarations = file_scoped:warning
csharp_prefer_braces = true:warning
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
dotnet_style_require_accessibility_modifiers = always:warning
csharp_style_prefer_pattern_matching = true:suggestion
最佳实践总结
应当
- 使用
record表示 DTO、消息和领域实体 - 使用
readonly record struct表示值对象 - 使用
switch表达式进行模式匹配 - 启用并遵循可空引用类型规范
- 对所有 I/O 操作使用 async/await
- 在所有异步方法中接受
CancellationToken - 在高性能场景中使用
Span<T>和Memory<T> - 接受抽象类型(
IEnumerable<T>、IReadOnlyList<T>) - 返回适当的接口或具体类型
- 使用
Result<T, TError>表示预期错误 - 在库代码中使用
ConfigureAwait(false) - 对大型分配使用
ArrayPool<T>池化缓冲区 - 优先使用组合而非继承
- 避免在应用代码中使用抽象基类
不应当
- 在适合使用记录时,不要使用可变类
- 不要用类表示值对象(应使用
readonly record struct) - 不要创建深层继承层次结构
- 不要忽略可空引用类型警告
- 不要阻塞异步代码(
.Result、.Wait()) - 在
Span<byte>足够时,不要使用byte[] - 不要忘记
CancellationToken参数 - 不要从 API 返回可变集合
- 不要为预期的业务错误抛出异常
- 不要在循环中拼接
string - 不要反复分配大型数组(应使用
ArrayPool)
知识来源
本技能中的约定基于以下公开内容:
- Microsoft Framework Design Guidelines —— .NET 命名、类型设计和 API 表面约定的权威参考。来源:https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/
- C# Language Design Notes (Mads Torgersen et al.) —— 影响编码标准的 C# 语言功能背后的设计原理。与本技能相关的重要决策包括:文件范围命名空间(减少嵌套,提升可读性)、优先使用模式匹配而非类型检查(表达力更强)、
required成员(确保编译时初始化安全)以及var使用指南(优先考虑可读性)。来源:https://github.com/dotnet/csharplang/tree/main/meetings
其他资源
- C# Language Specification:https://learn.microsoft.com/en-us/dotnet/csharp/
- Pattern Matching:https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching
- Span and Memory:https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/
- Async Best Practices:https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
- .NET Performance Tips:https://learn.microsoft.com/en-us/dotnet/framework/performance/
- C# Coding Conventions:https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
- C# Identifier Naming Rules:https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
- .editorconfig for .NET:https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options