现代C#编码标准
何时使用此技能
使用此技能时:
- 编写新的C#代码或重构现有代码
- 设计库或服务的公共API
- 优化性能关键代码路径
- 使用强类型实现领域模型
- 构建async/await重的应用程序
- 处理二进制数据、缓冲区或高吞吐量场景
核心原则
- 默认不可变性 - 使用
record类型和init-only属性 - 类型安全性 - 利用可空引用类型和值对象
- 现代模式匹配 - 广泛使用
switch表达式和模式 - 异步无处不在 - 优先选择带有适当取消支持的异步API
- 零分配模式 - 对于性能关键代码,使用
Span<T>和Memory<T> - API设计 - 接受抽象,返回适当具体的类型
- 组合优于继承 - 避免抽象基类,优先选择组合
- 值对象作为结构体 - 对于值对象使用
readonly record struct
语言模式
用于不可变数据的记录(C# 9+)
对DTO、消息、事件和领域实体使用record类型。
// 简单的不可变DTO
public record CustomerDto(string Id, string Name, string Email);
// 构造函数中带有验证的记录
public record EmailAddress
{
public string Value { get; init; }
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
throw new ArgumentException("无效的电子邮件地址", nameof(value));
Value = value;
}
}
// 带有计算属性的记录
public record Order(string Id, decimal Subtotal, decimal Tax)
{
public decimal Total => Subtotal + Tax;
}
// 带有集合的记录 - 使用IReadOnlyList
public record ShoppingCart(
string CartId,
string CustomerId,
IReadOnlyList<CartItem> Items
)
{
public decimal Total => Items.Sum(item => item.Price * item.Quantity);
}
何时使用record class与record struct:
record class(默认):引用类型,用于实体、聚合、具有多个属性的DTOrecord struct:值类型,用于值对象(见下一节)
值对象作为readonly record struct
值对象应该**始终是readonly record struct**以获得性能和值语义。
// 单值对象
public readonly record struct OrderId(string Value)
{
public OrderId(string value) : this(
!string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("OrderId不能为空", nameof(value)))
{
}
public override string ToString() => Value;
// 无隐式转换 - 破坏类型安全性!
// 显式访问内部值:orderId.Value
}
// 多值对象
public readonly record struct Money(decimal Amount, string Currency)
{
public Money(decimal amount, string currency) : this(
amount >= 0 ? amount : throw new ArgumentException("金额不能为负", nameof(amount)),
ValidateCurrency(currency))
{
}
private static string ValidateCurrency(string currency)
{
if (string.IsNullOrWhiteSpace(currency) || currency.Length != 3)
throw new ArgumentException("货币必须是一个3字母代码", nameof(currency));
return currency.ToUpperInvariant();
}
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException($"不能将{Currency}加到{other.Currency}");
return new Money(Amount + other.Amount, Currency);
}
public override string ToString() => $"{Amount:N2} {Currency}";
}
// 具有工厂模式的复杂值对象
public readonly record struct PhoneNumber
{
public string Value { get; }
private PhoneNumber(string value) => Value = value;
public static Result<PhoneNumber, string> Create(string input)
{
if (string.IsNullOrWhiteSpace(input))
return Result<PhoneNumber, string>.Failure("电话号码不能为空");
// 规范化:移除所有非数字
var digits = new string(input.Where(char.IsDigit).ToArray());
if (digits.Length < 10 || digits.Length > 15)
return Result<PhoneNumber, string>.Failure("电话号码必须是10-15位数字");
return Result<PhoneNumber, string>.Success(new PhoneNumber(digits));
public override string ToString() => Value;
}
// 百分比值对象带范围验证
public readonly record struct Percentage
{
private readonly decimal _value;
public decimal Value => _value;
public Percentage(decimal value)
{
if (value < 0 || value > 100)
throw new ArgumentOutOfRangeException(nameof(value), "百分比必须在0到100之间");
_value = value;
}
public decimal AsDecimal() => _value / 100m;
public static Percentage FromDecimal(decimal decimalValue)
{
if (decimalValue < 0 || decimalValue > 1)
throw new ArgumentOutOfRangeException(nameof(decimalValue), "小数必须在0和1之间");
return new Percentage(decimalValue * 100);
}
public override string ToString() => $"{_value}%";
}
// 强类型ID
public readonly record struct CustomerId(Guid Value)
{
public static CustomerId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
// 带单位的数量
public readonly record struct Quantity(int Value, string Unit)
{
public Quantity(int value, string unit) : this(
value >= 0 ? value : throw new ArgumentException("数量不能为负"),
!string.IsNullOrWhiteSpace(unit) ? unit : throw new ArgumentException("单位不能为空"))
{
}
public override string ToString() => $"{Value} {Unit}";
}
为什么对值对象使用readonly record struct:
- 值语义:基于内容的等式,而不是引用
- 栈分配:更好的性能,没有GC压力
- 不可变性:
readonly防止意外变异 - 模式匹配:与switch表达式无缝协作
**至关重要:无隐式转换。**隐式运算符破坏了值对象的目的,允许静默类型强制:
// 错误 - 破坏编译时安全性:
public readonly record struct UserId(Guid Value)
{
public static implicit operator UserId(Guid value) => new(value); // 不!
public static implicit operator Guid(UserId value) => value.Value; // 不!
}
// 有隐式运算符时,这会无声息地编译:
void ProcessUser(UserId userId) { }
ProcessUser(Guid.NewGuid()); // 哎呀 - 打算传递PostId
// 正确 - 所有转换都是显式的:
public readonly record struct UserId(Guid Value)
{
public static UserId New() => new(Guid.NewGuid());
// 没有隐式运算符
// 创建:new UserId(guid)或UserId.New()
// 提取:userId.Value
}
显式转换迫使每个边界交叉都可见:
// API边界 - 显式转换IN
var userId = new UserId(request.UserId); // 入口验证
// 数据库边界 - 显式转换OUT
await _db.ExecuteAsync(sql, new { UserId = userId.Value });
模式匹配(C# 8-12)
利用现代模式匹配获得更清晰、更富有表现力的代码。
// 带有值对象的switch表达式
public string GetPaymentMethodDescription(PaymentMethod payment) => payment switch
{
{ Type: PaymentType.CreditCard, Last4: var last4 } => $"信用卡以{last4}结尾",
{ Type: PaymentType.BankTransfer, AccountNumber: var account } => $"银行转账来自{account}",
{ Type: PaymentType.Cash } => "现金支付",
_ => "未知支付方式"
};
// 属性模式
public decimal CalculateDiscount(Order order) => order switch
{
{ Total: > 1000m } => order.Total * 0.15m,
{ Total: > 500m } => order.Total * 0.10m,
{ Total: > 100m } => order.Total * 0.05m,
_ => 0m
};
// 关系和逻辑模式
public string ClassifyTemperature(int temp) => temp switch
{
< 0 => "冻结",
>= 0 and < 10 => "冷",
>= 10 and < 20 => "凉爽",
>= 20 and < 30 => "温暖",
>= 30 => "热",
_ => throw new ArgumentOutOfRangeException(nameof(temp))
};
// 列表模式(C# 11+)
public bool IsValidSequence(int[] numbers) => numbers switch
{
[] => false, // 空
[_] => true, // 单个元素
[var first, .., var last] when first < last => true, // 第一个<最后一个
_ => false
};
// 带有null检查的类型模式
public string FormatValue(object? value) => value switch
{
null => "null",
string s => $"\"{s}\"",
int i => i.ToString(),
double d => d.ToString("F2"),
DateTime dt => dt.ToString("yyyy-MM-dd"),
Money m => m.ToString(),
IEnumerable<object> collection => $"[{string.Join(", ", collection)}]",
_ => value.ToString() ?? "未知"
};
// 结合模式进行复杂逻辑
public record OrderState(bool IsPaid, bool IsShipped, bool IsCancelled);
public string GetOrderStatus(OrderState state) => state switch
{
{ IsCancelled: true } => "已取消",
{ IsPaid: true, IsShipped: true } => "已交付",
{ IsPaid: true, IsShipped: false } => "处理中",
{ IsPaid: false } => "等待支付",
_ => "未知"
};
// 带有值对象的模式匹配
public decimal CalculateShipping(Money total, Country destination) => (total, destination) switch
{
({ Amount: > 100m }, _) => 0m, // 100美元以上免费送货
(_, { Code: "US" or "CA" }) => 5m, // 北美
(_, { Code: "GB" or "FR" or "DE" }) => 10m, // 欧洲
_ => 25m // 国际
};
可空引用类型(C# 8+)
在你的项目中启用可空引用类型,并显式处理null。
// 在.csproj中
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
// 显式空性
public class UserService
{
// 默认不可空
public string GetUserName(User user) => user.Name;
// 显式可空返回
public string? FindUserName(string userId)
{
var user = _repository.Find(userId);
return user?.Name; // 如果用户未找到则返回null
}
// Null-forgiving运算符(慎用!)
public string GetRequiredConfigValue(string key)
{
var value = Configuration[key];
return value!; // 只有在你确定它不为空时才使用
}
// 可空值对象
public Money? GetAccountBalance(string accountId)
{
var account = _repository.Find(accountId);
return account?.Balance;
}
}
// 带有null检查的模式匹配
public decimal GetDiscount(Customer? customer) => customer switch
{
null => 0m,
{ IsVip: true } => 0.20m,
{ OrderCount: > 10 } => 0.10m,
_ => 0.05m
};
// Null-coalescing模式
public string GetDisplayName(User? user) =>
user?.PreferredName ?? user?.Email ?? "Guest";
// 带有ArgumentNullException.ThrowIfNull的Guard子句(C# 11+)
public void ProcessOrder(Order? order)
{
ArgumentNullException.ThrowIfNull(order);
// 在此范围内order现在是不可空的
Console.WriteLine(order.Id);
}
组合优于继承
避免抽象基类和继承层次结构。 使用组合和接口代替。
// ❌ 不好:抽象基类层次结构
public abstract class PaymentProcessor
{
public abstract Task<PaymentResult> ProcessAsync(Money amount);
protected async Task<bool> ValidateAsync(Money amount)
{
// 共享验证逻辑
return amount.Amount > 0;
}
}
public class CreditCardProcessor : PaymentProcessor
{
public override async Task<PaymentResult> ProcessAsync(Money amount)
{
await ValidateAsync(amount);
// 处理信用卡...
}
}
// ✅ 好:组合接口
public interface IPaymentProcessor
{
Task<PaymentResult> ProcessAsync(Money amount, CancellationToken cancellationToken);
}
public interface IPaymentValidator
{
Task<ValidationResult> ValidateAsync(Money amount, CancellationToken cancellationToken);
}
// 具体实现组合验证器
public sealed class CreditCardProcessor : IPaymentProcessor
{
private readonly IPaymentValidator _validator;
private readonly ICreditCardGateway _gateway;
public CreditCardProcessor(IPaymentValidator validator, ICreditCardGateway gateway)
{
_validator = validator;
_gateway = gateway;
}
public async Task<PaymentResult> ProcessAsync(Money amount, CancellationToken cancellationToken)
{
var validation = await _validator.ValidateAsync(amount, cancellationToken);
if (!validation.IsValid)
return PaymentResult.Failed(validation.Error);
return await _gateway.ChargeAsync(amount, cancellationToken);
}
}
// ✅ 好:静态帮助类共享逻辑(无继承)
public static class PaymentValidation
{
public static ValidationResult ValidateAmount(Money amount)
{
if (amount.Amount <= 0)
return ValidationResult.Invalid("金额必须为正");
if (amount.Amount > 10000m)
return ValidationResult.Invalid("金额超出最大值");
return ValidationResult.Valid();
}
}
// ✅ 好:记录建模变体(不继承)
public enum PaymentType { CreditCard, BankTransfer, Cash }
public record PaymentMethod
{
public PaymentType Type { get; init; }
public string? Last4 { get; init; } // 用于信用卡
public string? AccountNumber { get; init; } // 用于银行转账
public static PaymentMethod CreditCard(string last4) => new()
{
Type = PaymentType.CreditCard,
Last4 = last4
};
public static PaymentMethod BankTransfer(string accountNumber) => new()
{
Type = PaymentType.BankTransfer,
AccountNumber = accountNumber
};
public static PaymentMethod Cash() => new() { Type = PaymentType.Cash };
}
继承是可以接受的:
- 框架要求(例如ASP.NET Core中的
ControllerBase) - 库集成(例如从
Exception继承的自定义异常) - 这些应该是你的应用程序代码中的罕见情况
性能模式
Async/Await最佳实践
始终对I/O绑定操作使用异步:
// ✅ 好:异步到底
public async Task<Order> GetOrderAsync(string orderId, CancellationToken cancellationToken)
{
var order = await _repository.GetAsync(orderId, cancellationToken);
var customer = await _customerService.GetCustomerAsync(order.CustomerId, cancellationToken);
return order;
}
// ❌ 不好:在异步代码上阻塞
public Order GetOrder(string orderId)
{
return _repository.GetAsync(orderId).Result; // 死锁风险!
}
// ✅ 好:频繁调用的方法使用ValueTask
public ValueTask<Order?> GetCachedOrderAsync(string orderId, CancellationToken cancellationToken)
{
if (_cache.TryGetValue(orderId, out var order))
return ValueTask.FromResult<Order?>(order); // 同步路径,无分配
return GetFromDatabaseAsync(orderId, cancellationToken); // 异步路径
}
private async ValueTask<Order?> GetFromDatabaseAsync(string orderId, CancellationToken cancellationToken)
{
var order = await _repository.GetAsync(orderId, cancellationToken);
if (order is not null)
_cache[orderId] = order;
return order;
}
// ✅ 好:流式IAsyncEnumerable
public async IAsyncEnumerable<Order> StreamOrdersAsync(
string customerId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var order in _repository.StreamAllAsync(cancellationToken))
{
if (order.CustomerId == customerId)
yield return order;
}
}
// ✅ 好:库代码中的ConfigureAwait(false)(不是应用程序代码)
public async Task<string> ProcessDataAsync(string input, CancellationToken cancellationToken)
{
var data = await FetchDataAsync(cancellationToken).ConfigureAwait(false);
var result = await TransformDataAsync(data, cancellationToken).ConfigureAwait(false);
return result;
}
始终接受CancellationToken:
// ✅ 好:带有默认的CancellationToken参数
public async Task<List<Order>> GetOrdersAsync(
string customerId,
CancellationToken cancellationToken = default)
{
var orders = await _repository.GetOrdersByCustomerAsync(customerId, cancellationToken);
return orders;
}
// 通过调用堆栈传递取消
public async Task<OrderSummary> GetOrderSummaryAsync(
string customerId,
CancellationToken cancellationToken = default)
{
var orders = await GetOrdersAsync(customerId, cancellationToken);
var total = orders.Sum(o => o.Total);
return new OrderSummary(customerId, orders.Count, total);
}
// 组合操作时链接取消令牌
public async Task<ProcessResult> ProcessWithTimeoutAsync(
string data,
TimeSpan timeout,
CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeout);
return await ProcessAsync(data, cts.Token);
}
Span<T>和Memory<T>零分配代码
对性能关键代码,使用Span<T>和Memory<T>代替byte[]或string。
// ✅ 好:Span<T>用于同步、零分配操作
public int ParseOrderId(ReadOnlySpan<char> input)
{
// 不分配工作数据
if (!input.StartsWith("ORD-"))
throw new FormatException("无效的订单ID格式");
var numberPart = input.Slice(4);
return int.Parse(numberPart);
}
// stackalloc与Span<T>
public void FormatMessage()
{
Span<char> buffer = stackalloc char[256];
var written = FormatInto(buffer);
var message = new string(buffer.Slice(0, written));
}
// SkipLocalsInit与stackalloc - 跳过零初始化以提高性能
// 默认情况下,.NET会零初始化所有局部变量(.locals init标志)。这可能
// 对stackalloc有可衡量的开销。当:
// - 你在读取之前写入缓冲区(如下面的FormatInto)
// - 性能分析显示零初始化是瓶颈
// ⚠️ 警告:在写入之前读取会返回垃圾数据(见文档示例)
// 需要:<AllowUnsafeBlocks>true</AllowUnsafeBlocks>在.csproj中
// 见:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/general#skiplocalsinit-attribute
using System.Runtime.CompilerServices;
[SkipLocalsInit]
public void FormatMessage()
{
Span<char> buffer = stackalloc char[256];
var written = FormatInto(buffer);
var message = new string(buffer.Slice(0, written));
}
// ✅ 好:异步操作的Memory<T>(Span不能跨越await边界)
public async Task<int> ReadDataAsync(
Memory<byte> buffer,
CancellationToken cancellationToken)
{
return await _stream.ReadAsync(buffer, cancellationToken);
}
// ✅ 好:避免分配的字符串操作与Span
public bool TryParseKeyValue(ReadOnlySpan<char> line, out string key, out string value)
{
key = string.Empty;
value = string.Empty;
int colonIndex = line.IndexOf(':');
if (colonIndex == -1)
return false;
// 只有在我们知道格式有效时才分配字符串
key = new string(line.Slice(0, colonIndex).Trim());
value = new string(line.Slice(colonIndex + 1).Trim());
return true;
}
// ✅ 好:ArrayPool临时大缓冲区
public async Task ProcessLargeFileAsync(
Stream stream,
CancellationToken cancellationToken)
{
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(), cancellationToken)) > 0)
{
ProcessChunk(buffer.AsSpan(0, bytesRead));
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
// 混合缓冲区模式用于短暂的UTF-8工作。见相应部分的SkipLocalsInit注意事项。
[SkipLocalsInit]
static short GenerateHashCode(string? key)
{
if (key is null) return 0;
const int StackLimit = 256;
var enc = Encoding.UTF8;
var max = enc.GetMaxByteCount(key.Length);
byte[]? rented = null;
Span<byte> buf = max <= StackLimit
? stackalloc byte[StackLimit]
: (rented = ArrayPool<byte>.Shared.Rent(max));
try
{
var written = enc.GetBytes(key.AsSpan(), buf);
ComputeHash(buf[..written], out var h1, out var h2);
return unchecked((short)(h1 ^ h2));
}
finally
{
if (rented is not null) ArrayPool<byte>.Shared.Return(rented);
}
}
// ✅ 好:无子字符串分配的基于Span的解析
public static (string Protocol, string Host, int Port) ParseUrl(ReadOnlySpan<char> url)
{
var protocolEnd = url.IndexOf("://");
var protocol = new string(url.Slice(0, protocolEnd));
var afterProtocol = url.Slice(protocolEnd + 3);
var portStart = afterProtocol.IndexOf(':');
var host = new string(afterProtocol.Slice(0, portStart));
var portSpan = afterProtocol.Slice(portStart + 1);
var port = int.Parse(portSpan);
return (protocol, host, port);
}
// ✅ 好:向Span写数据
public bool TryFormatOrderId(int orderId, Span<char> destination, out int charsWritten)
{
const string prefix = "ORD-";
if (destination.Length < prefix.Length + 10)
{
charsWritten = 0;
return false;
}
prefix.AsSpan().CopyTo(destination);
var numberWritten = orderId.TryFormat(
destination.Slice(prefix.Length),
out var numberChars);
charsWritten = prefix.Length + numberChars;
return numberWritten;
}
何时使用什么:
| 类型 | 用例 |
|---|---|
Span<T> |
同步操作,栈分配缓冲区,无分配切片 |
ReadOnlySpan<T> |
只读视图,你不会修改的数据的方法参数 |
Memory<T> |
异步操作(Span不能跨越await边界) |
ReadOnlyMemory<T> |
只读异步操作 |
byte[] |
当你需要长期存储数据或传递给需要数组的API时 |
ArrayPool<T> |
大型临时缓冲区(>1KB)以减少GC压力 |
API设计原则
接受抽象,返回适当具体
对于参数(接受):
// ✅ 好:如果你只迭代一次,接受IEnumerable<T>
public decimal CalculateTotal(IEnumerable<OrderItem> items)
{
return items.Sum(item => item.Price * item.Quantity);
}
// ✅ 好:如果你需要计数,接受IReadOnlyCollection<T>
public bool HasMinimumItems(IReadOnlyCollection<OrderItem> items, int minimum)
{
return items.Count >= minimum;
}
// ✅ 好:如果你需要索引,接受IReadOnlyList<T>
public OrderItem GetMiddleItem(IReadOnlyList<OrderItem> items)
{
if (items.Count == 0)
throw new ArgumentException("列表不能为空");
return items[items.Count / 2]; // 索引访问
}
// ✅ 好:对于高性能、零分配API,接受ReadOnlySpan<T>
public int Sum(ReadOnlySpan<int> numbers)
{
int total = 0;
foreach (var num in numbers)
total += num;
return total;
}
// ✅ 好:对于异步流,接受IAsyncEnumerable<T>
public async Task<int> CountItemsAsync(
IAsyncEnumerable<Order> orders,
CancellationToken cancellationToken)
{
int count = 0;
await foreach (var order in orders.WithCancellation(cancellationToken))
count++;
return count;
}
对于返回类型:
// ✅ 好:对于懒加载/延迟执行,返回IEnumerable<T>
public IEnumerable<Order> GetOrdersLazy(string customerId)
{
foreach (var order in _repository.Query())
{
if (order.CustomerId == customerId)
yield return order; // 懒加载评估
}
}
// ✅ 好:对于具体化、不可变集合,返回IReadOnlyList<T>
public IReadOnlyList<Order> GetOrders(string customerId)
{
return _repository
.Query()
.Where(o => o.CustomerId == customerId)
.ToList(); // 具体化
}
// ✅ 好:当调用者需要变异时,返回具体类型
public List<Order> GetMutableOrders(string customerId)
{
// 明确允许变异,返回List<T>
return _repository
.Query()
.Where(o => o.CustomerId == customerId)
.ToList();
}
// ✅ 好:对于异步流,返回IAsyncEnumerable<T>
public async IAsyncEnumerable<Order> StreamOrdersAsync(
string customerId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var order in _repository.StreamAllAsync(cancellationToken))
{
if (order.CustomerId == customerId)
yield return order;
}
}
// ✅ 好:对于互操作或当调用者期望数组时,返回数组
public byte[] SerializeOrder(Order order)
{
// 二进制序列化 - byte[]在这里是合适的
return MessagePackSerializer.Serialize(order);
}
总结表:
| 场景 | 接受 | 返回 |
|---|---|---|
| 只迭代一次 | IEnumerable<T> |
IEnumerable<T>(如果是懒加载) |
| 需要计数 | IReadOnlyCollection<T> |
IReadOnlyCollection<T> |
| 需要索引 | IReadOnlyList<T> |
IReadOnlyList<T> |
| 高性能,同步 | ReadOnlySpan<T> |
Span<T>(很少) |
| 异步流 | IAsyncEnumerable<T> |
IAsyncEnumerable<T> |
| 调用者需要变异 | - | List<T>, T[] |
方法签名最佳实践
// ✅ 好:完整的异步方法签名
public async Task<Result<Order, OrderError>> CreateOrderAsync(
CreateOrderRequest request,
CancellationToken cancellationToken = default)
{
// 实现
}
// ✅ 好:可选参数在最后
public async Task<List<Order>> GetOrdersAsync(
string customerId,
DateTime? startDate = null,
DateTime? endDate = null,
CancellationToken cancellationToken = default)
{
// 实现
}
// ✅ 好:用于多个相关参数的记录
public record SearchOrdersRequest(
string? CustomerId,
DateTime? StartDate,
DateTime? EndDate,
OrderStatus? Status,
int PageSize = 20,
int PageNumber = 1
);
public async Task<PagedResult<Order>> SearchOrdersAsync(
SearchOrdersRequest request,
CancellationToken cancellationToken = default)
{
// 实现
}
// ✅ 好:简单类的主构造函数(C# 12+)
public sealed class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
public async Task<Order> GetOrderAsync(OrderId orderId, CancellationToken cancellationToken)
{
logger.LogInformation("正在获取订单{OrderId}", orderId);
return await repository.GetAsync(orderId, cancellationToken);
}
}
// ✅ 好:复杂配置的选项模式
public sealed class EmailServiceOptions
{
public required string SmtpHost { get; init; }
public int SmtpPort { get; init; } = 587;
public bool UseSsl { get; init; } = true;
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
}
public sealed class EmailService(IOptions<EmailServiceOptions> options)
{
private readonly EmailServiceOptions _options = options.Value;
}
错误处理
结果类型模式(面向铁路的编程)
对于预期错误,使用Result<T, TError>类型而不是异常。
// 简单的Result类型作为readonly记录结构
public readonly record struct Result<TValue, TError>
{
private readonly TValue? _value;
private readonly TError? _error;
private readonly bool _isSuccess;
private Result(TValue value)
{
_value = value;
_error = default;
_isSuccess = true;
}
private Result(TError error)
{
_value = default;
_error = error;
_isSuccess = false;
}
public bool IsSuccess => _isSuccess;
public bool IsFailure => !_isSuccess;
public TValue Value => _isSuccess
? _value!
: throw new InvalidOperationException("不能访问失败结果的值");
public TError Error => !_isSuccess
? _error!
: throw new InvalidOperationException("不能访问成功结果的错误");
public static Result<TValue, TError> Success(TValue value) => new(value);
public static Result<TValue, TError> Failure(TError error) => new(error);
public Result<TOut, TError> Map<TOut>(Func<TValue, TOut> mapper)
=> _isSuccess
? Result<TOut, TError>.Success(mapper(_value!))
: Result<TOut, TError>.Failure(_error!);
public Result<TOut, TError> Bind<TOut>(Func<TValue, Result<TOut, TError>> binder)
=> _isSuccess ? binder(_value!) : Result<TOut, TError>.Failure(_error!);
public TValue GetValueOr(TValue defaultValue)
=> _isSuccess ? _value! : defaultValue;
public TResult Match<TResult>(
Func<TValue, TResult> onSuccess,
Func<TError, TResult> onFailure)
=> _isSuccess ? onSuccess(_value!) : onFailure(_error!);
}
// 错误类型作为readonly记录结构
public readonly record struct OrderError(string Code, string Message);
// 使用示例
public sealed class OrderService(IOrderRepository repository)
{
public async Task<Result<Order, OrderError>> CreateOrderAsync(
CreateOrderRequest request,
CancellationToken cancellationToken)
{
// 验证
var validationResult = ValidateRequest(request);
if (validationResult.IsFailure)
return Result<Order, OrderError>.Failure(validationResult.Error);
// 检查库存
var inventoryResult = await CheckInventoryAsync(request.Items, cancellationToken);
if (inventoryResult.IsFailure)
return Result<Order, OrderError>.Failure(inventoryResult.Error);
// 创建订单
var order = new Order(
OrderId.New(),
new CustomerId(request.CustomerId),
request.Items);
await repository.SaveAsync(order, cancellationToken);
return Result<Order, OrderError>.Success(order);
}
// 模式匹配Result
public IActionResult MapToActionResult(Result<Order, OrderError> result)
{
return result.Match(
onSuccess: order => new OkObjectResult(order),
onFailure: error => error.Code switch
{
"VALIDATION_ERROR" => new BadRequestObjectResult(error.Message),
"INSUFFICIENT_INVENTORY" => new ConflictObjectResult(error.Message),
"NOT_FOUND" => new NotFoundObjectResult(error.Message),
_ => new ObjectResult(error.Message) { StatusCode = 500 }
}
);
}
}
何时使用Result与异常:
- 使用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);
}
// 使用'with'表达式进行测试变化
[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")); // 1500的15%
}
// 基于Span的测试
[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("*不同货币*");
}
避免反射式元编程
优先选择静态类型、显式代码而不是反射式"魔法"库。
反射式库如AutoMapper牺牲编译时安全性以换取便利。当映射失败时,你会在运行时(或更糟,在生产中)而不是编译时发现。
禁止库
| 库 | 问题 |
|---|---|
| AutoMapper | 反射魔法,隐藏映射,运行时失败,难以调试 |
| Mapster | 与AutoMapper相同的问题 |
| ExpressMapper | 与AutoMapper相同的问题 |
为什么反射映射失败
// 使用AutoMapper - 编译得很好,运行时失败
public record UserDto(string Id, string Name, string Email);
public record UserEntity(Guid Id, string FullName, string EmailAddress);
// 这个映射无声息地产生垃圾:
// - Id:字符串与Guid不匹配
// - Name与FullName:无匹配,null/default
// - Email与EmailAddress:无匹配,null/default
var dto = _mapper.Map<UserDto>(entity); // 编译!在运行时中断。
使用显式映射方法代替
// 扩展方法 - 编译时检查,易于查找,易于调试
public static class UserMappings
{
public static UserDto ToDto(this UserEntity entity) => new(
Id: entity.Id.ToString(),
Name: entity.FullName,
Email: entity.EmailAddress);
public static UserEntity ToEntity(this CreateUserRequest request) => new(
Id: Guid.NewGuid(),
FullName: request.Name,
EmailAddress: request.Email);
}
// 使用 - 显式和可追溯
var dto = entity.ToDto();
var entity = request.ToEntity();
显式映射的好处
| 方面 | AutoMapper | 显式方法 |
|---|---|---|
| 编译时安全性 | 否 - 运行时错误 | 是 - 编译器捕获不匹配 |
| 可发现性 | 隐藏在配置文件中 | "Go to Definition"有效 |
| 调试 | 黑箱 | 逐步通过代码 |
| 重构 | 重命名静默失败 | IDE正确重命名 |
| 性能 | 反射开销 | 直接属性访问 |
| 测试 | 需要集成测试 | 简单的单元测试 |
复杂映射
对于复杂的转换,显式代码更有价值:
public static OrderSummaryDto ToSummary(this Order order) => new(
OrderId: order.Id.Value.ToString(),
CustomerName: order.Customer.FullName,
ItemCount: order.Items.Count,
Total: order.Items.Sum(i => i.Quantity * i.UnitPrice),
Status: order.Status switch
{
OrderStatus.Pending => "等待支付",
OrderStatus.Paid => "处理中",
OrderStatus.Shipped => "在路上",
OrderStatus.Delivered => "已完成",
_ => "未知"
},
FormattedDate: order.CreatedAt.ToString("MMMM d, yyyy"));
这是:
- 可读性:任何人都能理解转换
- 可调试性:设置断点,检查值
- 可测试性:传递一个Order,断言结果
- 可重构性:更改属性名称,编译器告诉您它在哪里使用
何时反射是可以接受的
反射有合法用途,但映射DTO不是其中之一:
| 用例 | 可以接受? |
|---|---|
| 序列化(System.Text.Json, Newtonsoft) | 是 - 经过充分测试,有源生成器 |
| 依赖注入容器 | 是 - 框架基础设施 |
| ORM实体映射(EF Core) | 是 - 必要的数据库抽象 |
| 测试固定装置和构建器 | 有时 - 仅在测试中的便利 |
| DTO/领域对象映射 | 不 - 使用显式方法 |
UnsafeAccessorAttribute(.NET 8+)
当你真的需要访问私有或内部成员(序列化器、测试帮助器、框架代码)时,使用UnsafeAccessorAttribute而不是传统反射。它提供了零开销,AOT兼容的成员访问。
// 避免:传统反射 - 慢,分配,破坏AOT
var field = typeof(Order).GetField("_status", BindingFlags.NonPublic | BindingFlags.Instance);
var status = (OrderStatus)field!.GetValue(order)!;
// 更喜欢:UnsafeAccessor - 零开销,AOT兼容
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_status")]
static extern ref OrderStatus GetStatusField(Order order);
var status = GetStatusField(order); // 直接访问,无反射
支持的访问器类型:
// 私有字段访问
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
static extern ref List<OrderItem> GetItemsField(Order order);
// 私有方法访问
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "Recalculate")]
static extern void CallRecalculate(Order order);
// 私有静态字段
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "_instanceCount")]
static extern ref int GetInstanceCount(Order order);
// 私有构造函数
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
static extern Order CreateOrder(OrderId id, CustomerId customerId);
为什么UnsafeAccessor比反射好:
| 方面 | 反射 | UnsafeAccessor |
|---|---|---|
| 性能 | 慢(100-1000倍) | 零开销 |
| AOT兼容 | 否 | 是 |
| 分配 | 是(装箱,数组) | 无 |
| 编译时检查 | 否 | 部分(签名) |
用例:
- 序列化器访问私有支持字段
- 测试帮助器验证内部状态
- 需要绕过可见性的框架代码
资源:
避免反模式
❌ 不要:使用可变DTO
// 坏:可变DTO
public class CustomerDto
{
public string Id { get; set; }
public string Name { get; set; }
}
// 好:不可变记录
public record CustomerDto(string Id, string Name);
❌ 不要:将值对象作为类
// 坏:值对象作为类
public class OrderId
{
public string Value { get; }
public OrderId(string value) => Value = value;
}
// 好:值对象作为readonly记录结构
public readonly record struct OrderId(string Value);
❌ 不要:创建深层继承层次结构
// 坏:深层继承
public abstract class Entity { }
public abstract class AggregateRoot : Entity { }
public abstract class Order : AggregateRoot { }
public class CustomerOrder : Order { }
// 好:平面结构与组合
public interface IEntity
{
Guid Id { get; }
}
public record Order(OrderId Id, CustomerId CustomerId, Money Total) : IEntity
{
Guid IEntity.Id => Id.Value;
}
❌ 不要:当你意味着IReadOnlyList<T>时返回List<T>
// 坏:暴露内部列表以进行修改
public List<Order> GetOrders() => _orders;
// 好:返回只读视图
public IReadOnlyList<Order> GetOrders() => _orders;
❌ 不要:当ReadOnlySpan<byte>可以工作时使用byte[]
// 坏:每次调用都分配数组
public byte[] GetHeader()
{
var header = new byte[64];
// 填充标题
return header;
}
// 好:零分配与Span
public void GetHeader(Span<byte> destination)
{
if (destination.Length < 64)
throw new ArgumentException("缓冲区太小");
// 直接在调用者的缓冲区中填充标题
}
❌ 不要:忘记异步方法中的CancellationToken
// 坏:没有取消支持
public async Task<Order> GetOrderAsync(OrderId id)
{
return await _repository.GetAsync(id);
}
// 好:取消支持
public async Task<Order> GetOrderAsync(
OrderId id,
CancellationToken cancellationToken = default)
{
return await _repository.GetAsync(id, cancellationToken);
}
❌ 不要:在异步代码上阻塞
// 坏:死锁风险!
public Order GetOrder(OrderId id)
{
return GetOrderAsync(id).Result;
}
// 坏:也是死锁风险!
public Order GetOrder(OrderId id)
{
return GetOrderAsync(id).GetAwaiter().GetResult();
}
// 好:异步到底
public async Task<Order> GetOrderAsync(
OrderId id,
CancellationToken cancellationToken)
{
return await _repository.GetAsync(id, cancellationToken);
}
代码组织
// 文件:Domain/Orders/Order.cs
namespace MyApp.Domain.Orders;
// 1. 主要领域类型
public record Order(
OrderId Id,
CustomerId CustomerId,
Money Total,
OrderStatus Status,
IReadOnlyList<OrderItem> Items
)
{
// 计算属性
public bool IsCompleted => Status is OrderStatus.Completed;
// 返回Result的领域方法以处理预期错误
public Result<Order, OrderError> AddItem(OrderItem item)
{
if (Status is not OrderStatus.Draft)
return Result<Order, OrderError>.Failure(
new OrderError("ORDER_NOT_DRAFT", "只能向草稿订单添加项目"));
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 });
}
}
// 2. 状态枚举
public enum OrderStatus
{
Draft,
Submitted,
Processing,
Completed,
Cancelled
}
// 3. 相关类型
public record OrderItem(
ProductId ProductId,
Quantity Quantity,
Money UnitPrice
)
{
public Money Total => new(
UnitPrice.Amount * Quantity.Value,
UnitPrice.Currency);
}
// 4. 值对象
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
}
// 5. 错误
public readonly record struct OrderError(string Code, string Message);
最佳实践总结
做的✅
- 使用
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()) - 不要使用
byte[]当Span<byte>足够时 - 不要忘记
CancellationToken参数 - 不要从API返回可变集合
- 不要为预期的业务错误抛出异常
- 不要在循环中使用
string连接 - 不要重复分配大型数组(使用
ArrayPool)
其他资源
- C#语言规范:https://learn.microsoft.com/en-us/dotnet/csharp/
- 模式匹配:https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching
- Span<T>和Memory<T>:https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/
- 异步最佳实践:https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
- .NET性能提示:https://learn.microsoft.com/en-us/dotnet/framework/performance/