name: type-design-performance description: 为性能设计.NET类型。密封类,使用只读结构体,优先使用静态纯函数,避免过早枚举,并选择合适的集合类型。 invocable: false
为性能设计类型
何时使用此技能
在以下情况下使用此技能:
- 设计新类型和API时
- 审查代码以查找性能问题时
- 在类、结构体和记录之间进行选择时
- 处理集合和可枚举对象时
核心原则
- 密封你的类型 - 除非明确设计为用于继承
- 优先使用只读结构体 - 适用于小的、不可变的值类型
- 优先使用静态纯函数 - 更好的性能和可测试性
- 延迟枚举 - 在需要之前不要物化
- 返回不可变集合 - 从API边界返回
默认密封类
密封类支持JIT去虚拟化,并传达API意图。
// 做:密封非为继承设计的类
public sealed class OrderProcessor
{
public void Process(Order order) { }
}
// 做:密封记录(它们是类)
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
// 不要:没有理由地保持未密封
public class OrderProcessor // 可以被继承 - 是有意的吗?
{
public virtual void Process(Order order) { } // 虚方法 = 更慢
}
好处:
- JIT可以对方法调用进行去虚拟化
- 传达“这不是一个扩展点”
- 防止意外的破坏性更改
为值类型使用只读结构体
当结构体不可变时,应标记为 readonly。这可以防止防御性复制。
// 做:为不可变值类型使用只读结构体
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
// 做:为小的、短生命周期的数据使用只读结构体
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// 不要:可变结构体(导致防御性复制)
public struct Point // 不是只读!
{
public int X { get; set; } // 可变的!
public int Y { get; set; }
}
何时使用结构体
| 使用结构体当 | 使用类当 |
|---|---|
| 小(通常≤16字节) | 更大的对象 |
| 短生命周期 | 长生命周期 |
| 频繁分配 | 需要共享引用 |
| 需要值语义 | 需要标识语义 |
| 不可变 | 可变状态 |
优先使用静态纯函数
没有副作用的静态方法更快且更易于测试。
// 做:静态纯函数
public static class OrderCalculator
{
public static Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
var total = items.Sum(i => i.Price * i.Quantity);
return new Money(total, "USD");
}
}
// 用法 - 可预测,可测试
var total = OrderCalculator.CalculateTotal(items);
好处:
- 无需vtable查找(更快)
- 无隐藏状态
- 更易于测试(纯输入 → 输出)
- 设计上线程安全
- 强制显式依赖
// 不要:隐藏依赖项的实例方法
public class OrderCalculator
{
private readonly ITaxService _taxService; // 隐藏的依赖项
private readonly IDiscountService _discountService; // 隐藏的依赖项
public Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
// 这实际上依赖于什么?
}
}
// 更好:通过参数显式依赖
public static class OrderCalculator
{
public static Money CalculateTotal(
IReadOnlyList<OrderItem> items,
decimal taxRate,
decimal discountPercent)
{
// 所有输入可见
}
}
不要过度使用 - 当你确实需要状态或多态性时,请使用实例方法。
延迟枚举
在必要时才物化可枚举对象。避免过多的LINQ链。
// 不好:过早物化
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.ToList() // 物化了!
.OrderBy(o => o.CreatedAt) // 又一次迭代
.ToList(); // 再次物化!
}
// 好:延迟到最后
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt)
.ToList(); // 单次物化
}
// 好:如果调用者可能不需要所有项,则返回IEnumerable
public IEnumerable<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt);
// 调用者决定何时物化
}
异步枚举
小心处理异步和IEnumerable:
// 不好:LINQ中的异步 - 隐藏的分配
var results = orders
.Select(async o => await ProcessOrderAsync(o)) // 每个项一个Task!
.ToList();
await Task.WhenAll(results);
// 好:使用IAsyncEnumerable进行流式处理
public async IAsyncEnumerable<OrderResult> ProcessOrdersAsync(
IEnumerable<Order> orders,
[EnumeratorCancellation] CancellationToken ct = default)
{
foreach (var order in orders)
{
ct.ThrowIfCancellationRequested();
yield return await ProcessOrderAsync(order, ct);
}
}
// 好:用于并行处理的批处理
var results = await Task.WhenAll(
orders.Select(o => ProcessOrderAsync(o)));
ValueTask 与 Task
对于经常同步完成的热路径,使用 ValueTask。对于真正的I/O,直接使用 Task。
// 做:为缓存/同步路径使用ValueTask
public ValueTask<User?> GetUserAsync(UserId id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user); // 无分配
}
return new ValueTask<User?>(FetchUserAsync(id));
}
// 做:为真正的I/O使用Task(更简单,无陷阱)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
// 这总是会访问数据库
return _repository.CreateAsync(cmd);
}
ValueTask规则:
- 切勿多次等待同一个ValueTask
- 在完成之前切勿使用
.Result或.GetAwaiter().GetResult() - 如有疑问,使用Task
为字节使用 Span 和 Memory
对于低级操作,使用 Span<T> 和 Memory<T> 代替 byte[]。
// 做:为同步操作接受Span
public static int ParseInt(ReadOnlySpan<char> text)
{
return int.Parse(text);
}
// 做:为异步操作接受Memory
public async Task WriteAsync(ReadOnlyMemory<byte> data)
{
await _stream.WriteAsync(data);
}
// 不要:强制数组分配
public static int ParseInt(string text) // 分配了字符串
{
return int.Parse(text);
}
常见的Span模式
// 无分配切片
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
var hello = span[..5]; // 无分配
// 为小缓冲区进行栈分配
Span<byte> buffer = stackalloc byte[256];
// 为更大的缓冲区使用ArrayPool
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
// 使用缓冲区...
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
集合返回类型
从API返回不可变集合
// 做:返回不可变集合
public IReadOnlyList<Order> GetOrders()
{
return _orders.ToList(); // 调用者无法修改内部状态
}
// 做:为静态数据使用冻结集合(.NET 8+)
private static readonly FrozenDictionary<string, Handler> _handlers =
new Dictionary<string, Handler>
{
["create"] = new CreateHandler(),
["update"] = new UpdateHandler(),
}.ToFrozenDictionary();
// 不要:返回可变集合
public List<Order> GetOrders()
{
return _orders; // 调用者可以修改!
}
内部可变性是可以的
public IReadOnlyList<OrderItem> BuildOrderItems(Cart cart)
{
var items = new List<OrderItem>(); // 内部可变
foreach (var cartItem in cart.Items)
{
items.Add(CreateOrderItem(cartItem));
}
return items; // 作为IReadOnlyList返回
}
集合指南
| 场景 | 返回类型 |
|---|---|
| API边界 | IReadOnlyList<T>, IReadOnlyCollection<T> |
| 静态查找数据 | FrozenDictionary<K,V>, FrozenSet<T> |
| 内部构建 | List<T>,然后作为只读返回 |
| 单个项或无 | T?(可空) |
| 零个或多个,惰性 | IEnumerable<T> |
快速参考
| 模式 | 好处 |
|---|---|
sealed class |
去虚拟化,清晰的API |
readonly record struct |
无防御性复制,值语义 |
| 静态纯函数 | 无vtable,可测试,线程安全 |
延迟 .ToList() |
单次物化 |
为热路径使用 ValueTask |
避免Task分配 |
为字节使用 Span<T> |
栈分配,无复制 |
返回 IReadOnlyList<T> |
不可变的API契约 |
FrozenDictionary |
静态数据的最快查找 |
反模式
// 不要:没有理由的未密封类
public class OrderService { } // 密封它!
// 不要:可变结构体
public struct Point { public int X; public int Y; } // 设为只读
// 不要:可以是静态的实例方法
public int Add(int a, int b) => a + b; // 设为静态
// 不要:多次调用ToList()
items.Where(...).ToList().OrderBy(...).ToList(); // 最后调用一次ToList
// 不要:从公共API返回List<T>
public List<Order> GetOrders(); // 返回IReadOnlyList<T>
// 不要:为总是异步的操作使用ValueTask
public ValueTask<Order> CreateOrderAsync(); // 直接使用Task