关于 type-design-performance
面向 .NET 开发者的类型设计技能,提供围绕性能、内存分配与 API 形状的设计和审查指导。内容涵盖结构体、集合、枚举及异步数据处理等选择。
主要功能
- 01结构体与类的选择建议
- 02默认密封类型的指导
- 03只读结构体设计要点
- 04集合类型选择建议
- 05延迟枚举与异步流实践
- 06Span、Memory 与 ValueTask 用法
适用场景
- 01设计新的类型和 API
- 02审查代码中的性能问题
- 03选择 class、struct 或 record
- 04为集合及可枚举数据选择模式
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill type-design-performance安装后在 Claude Code 中输入/type-design-performance确认已加载。
详细介绍
面向性能的类型设计
何时使用此技能
在以下情况下使用此技能:
- 设计新类型和 API
- 检查代码中的性能问题
- 在 class、struct 和 record 之间进行选择
- 使用集合和可枚举对象
核心原则
- 密封类型——除非明确设计为可继承
- 优先使用只读结构体——适用于小型、不可变的值类型
- 优先使用静态纯函数——性能和可测试性更好
- 延迟枚举——在需要之前不要物化
- 返回不可变集合——从 API 边界返回
struct 与 class 决策矩阵
在设计时选择 struct 还是 class,会对分配、GC 压力和 API 形态产生连锁影响。
决策标准
| 标准 | 倾向于 struct | 倾向于 class |
|---|---|---|
| 大小 | 小型(理想情况下 <= 16 字节,可接受 <= 64 字节) | 大型或大小可变 |
| 生命周期 | 生命周期短,作用域限于方法 | 生命周期长,跨作用域共享 |
| 标识 | 值相等(数据相同的两个实例相等) | 引用标识很重要 |
| 可变性 | 不可变(readonly struct) | 可变或状态转换复杂 |
| 继承 | 不需要 | 需要多态或基类 |
| 可空语义 | default 是有效的零状态 | 需要显式的 null 表示缺失 |
| 集合用法 | 存储在数组/跨度中(连续内存) | 通过引用存储(间接访问) |
大小指南
<= 16 bytes: Ideal struct -- fits in two registers, passed efficiently
17-64 bytes: Acceptable struct -- measure copy cost vs allocation cost
> 64 bytes: Prefer class -- copying cost outweighs allocation avoidance
常见类型及其正确设计
| 类型 | 正确选择 | 原因 |
|---|---|---|
| Point2D(8 字节:两个浮点数) | readonly struct | 小型、不可变、值语义 |
| Money(16 字节:decimal + currency) | readonly struct | 小型、不可变、值相等 |
| DateRange(16 字节:两个 DateOnly) | readonly struct | 小型、不可变、值语义 |
| Matrix4x4(64 字节:16 个浮点数) | struct(使用 in 参数) | 对性能要求极高的数学运算 |
| CustomerDto(大小可变:字符串、列表) | class 或 record | 包含引用,大小可变 |
| HttpRequest 上下文 | class | 生命周期长,在中间件间共享 |
默认密封
为什么要密封库类型
对于库类型(供其他程序集使用的代码),默认应密封类:
- JIT 去虚拟化——密封类允许 JIT 将虚调用替换为直接调用,从而支持内联
- 契约更简单——未密封类意味着承诺支持继承
- 减少破坏性更改——之后再密封类会造成二进制兼容性破坏
// GOOD -- sealed by default for library types
public sealed class WidgetService
{
public Widget GetWidget(int id) => new(id, "Default");
}
// Only unseal when inheritance is an intentional design decision
public abstract class WidgetValidatorBase
{
public abstract bool Validate(Widget widget);
protected virtual void OnValidationComplete(Widget widget) { }
}
不要密封的情况
| 场景 | 原因 |
|---|---|
| 抽象基类 | 继承是其用途 |
| 框架扩展点 | 使用者需要创建子类 |
| 不可模拟设计中的测试替身 | 模拟框架需要创建子类 |
| 应用程序内部类 | 密封没有额外价值 |
只读结构体
当结构体的所有字段都不可变时,将其标记为 readonly。这样可以消除 JIT 在通过 in 参数或 readonly 字段访问结构体时创建的防御性副本。
防御性副本问题
// NON-readonly struct -- JIT must defensively copy on every method call
public struct MutablePoint
{
public double X;
public double Y;
public double Length() => Math.Sqrt(X * X + Y * Y);
}
public double GetLength(in MutablePoint point)
{
return point.Length(); // Hidden copy here!
}
// GOOD -- readonly struct: JIT knows no mutation is possible
public readonly struct ImmutablePoint
{
public double X { get; }
public double Y { get; }
public ImmutablePoint(double x, double y) => (X, Y) = (x, y);
public double Length() => Math.Sqrt(X * X + Y * Y);
}
public double GetLength(in ImmutablePoint point)
{
return point.Length(); // No copy, direct call
}
只读结构体检查清单
- 所有字段都是
readonly,或属性使用{ get; }/{ get; init; } - 没有方法会修改状态
- 构造函数会初始化所有字段
- 考虑实现
IEquatable<T>,以便在值比较时避免装箱
用于数据传输的 Record 类型
record class 与 record struct
| 特性 | record class | record struct |
|---|---|---|
| 分配位置 | 堆 | 栈(或内联存储在数组中) |
| 相等性 | 引用类型,具有值相等性 | 值类型,具有值相等性 |
with 表达式 | 创建新的堆对象 | 创建新的栈副本 |
| 可空性 | null 表示缺失 | default 表示空状态 |
| 大小 | 引用(x64 上为 8 字节)+ 堆 | 栈上的完整大小 |
// record class -- heap allocated, good for DTOs
public record CustomerDto(string Name, string Email, DateOnly JoinDate);
// readonly record struct -- stack allocated, good for small value objects
public readonly record struct Money(decimal Amount, string Currency);
优先使用静态纯函数
没有副作用的静态方法速度更快,也更易于测试。
// DO: Static pure function
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");
}
}
// Usage - predictable, testable
var total = OrderCalculator.CalculateTotal(items);
优点:
- 无需查找虚方法表(速度更快)
- 没有隐藏状态
- 更易于测试(纯输入 → 输出)
- 天然线程安全
- 强制显式声明依赖项
延迟枚举
只有在必要时才物化可枚举对象。避免过度使用 LINQ 链式调用。
// BAD: Premature materialization
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.ToList() // Materialized!
.OrderBy(o => o.CreatedAt) // Another iteration
.ToList(); // Materialized again!
}
// GOOD: Defer until the end
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt)
.ToList(); // Single materialization
}
// GOOD: Return IEnumerable if caller might not need all items
public IEnumerable<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt);
}
异步枚举
// GOOD: Use IAsyncEnumerable for streaming
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);
}
}
// GOOD: Batch processing for parallelism
var results = await Task.WhenAll(
orders.Select(o => ProcessOrderAsync(o)));
ValueTask 与 Task
对于经常同步完成的热点路径,使用 ValueTask。对于实际的 I/O,直接使用 Task。
// DO: ValueTask for cached/synchronous paths
public ValueTask<User?> GetUserAsync(UserId id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user); // No allocation
}
return new ValueTask<User?>(FetchUserAsync(id));
}
// DO: Task for real I/O (simpler, no footguns)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
return _repository.CreateAsync(cmd);
}
ValueTask 使用规则:
- 不要多次 await 同一个 ValueTask
- 完成前不要使用
.Result或.GetAwaiter().GetResult() - 如果不确定,就使用 Task
ref struct 与 Span/Memory 的选择
ref struct 的限制
ref struct 类型仅能存储在栈上:不能装箱、不能存储在非 ref struct 类型的字段中,也不能用于异步方法。
Span 与 Memory 的选择
| 条件 | 使用 Span<T> | 使用 Memory<T> |
|---|---|---|
| 同步方法 | 是 | 是(但 Span 开销更低) |
| 异步方法 | 否(ref struct) | 是 |
| 存储在字段/集合中 | 否(ref struct) | 是 |
| 传递给回调/委托 | 否 | 是 |
| 无分配切片 | 是 | 是 |
| 包装 stackalloc 缓冲区 | 是 | 否 |
选择流程图
Will the buffer be used in an async method or stored in a field?
YES -> Use Memory<T> (convert to Span<T> with .Span for synchronous processing)
NO -> Do you need to wrap a stackalloc buffer?
YES -> Use Span<T>
NO -> Prefer Span<T> for lowest overhead
实用模式
// Public API uses Memory<T> for maximum flexibility
public async Task<int> ProcessAsync(ReadOnlyMemory<byte> data,
CancellationToken ct = default)
{
await _stream.WriteAsync(data, ct);
return CountNonZero(data.Span);
}
// Internal hot-path method uses Span<T> for zero overhead
private static int CountNonZero(ReadOnlySpan<byte> data)
{
var count = 0;
foreach (var b in data)
{
if (b != 0) count++;
}
return count;
}
常见 Span 模式
// Slice without allocation
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
var hello = span[..5]; // No allocation
// Stack allocation for small buffers
Span<byte> buffer = stackalloc byte[256];
// Use ArrayPool for larger buffers
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
// Use buffer...
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
集合类型选择
决策矩阵
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 构建一次,读取多次 | FrozenDictionary<K,V> / FrozenSet<T> | 优化读取布局(.NET 8+) |
| 构建一次,读取多次(.NET 8 之前) | ImmutableDictionary<K,V> | 线程安全、不可变 |
| 并发读写 | ConcurrentDictionary<K,V> | 无需外部锁定即可实现线程安全 |
| 频繁修改 | Dictionary<K,V> | 每次操作的开销最低 |
| 有序数据 | SortedDictionary<K,V> | O(log n) 查找,枚举结果有序 |
| 从公共 API 返回 | IReadOnlyList<T> / IReadOnlyDictionary<K,V> | 不可变接口 |
| 栈分配的小型集合 | 使用 stackalloc 的 Span<T> | 不产生 GC 压力 |
FrozenDictionary(.NET 8+)
FrozenDictionary<K,V> 会在创建时优化内部布局,以实现最高的读取性能:
using System.Collections.Frozen;
private static readonly FrozenDictionary<string, int> StatusCodes =
new Dictionary<string, int>
{
["OK"] = 200,
["NotFound"] = 404,
["InternalServerError"] = 500
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
public int GetStatusCode(string name) =>
StatusCodes.TryGetValue(name, out var code) ? code : -1;
适用场景:
- 启动时填充的配置查找表
- 静态映射(枚举到字符串、错误代码、功能标志)
- 任何只填充一次、读取多次的字典
不适用场景:
- 运行时会变化的数据
- 优化开销无法收回的小型查找(< 10 项)
集合返回类型
// DO: Return immutable collection
public IReadOnlyList<Order> GetOrders()
{
return _orders.ToList();
}
// DO: Use frozen collections for static data
private static readonly FrozenDictionary<string, Handler> _handlers =
new Dictionary<string, Handler>
{
["create"] = new CreateHandler(),
["update"] = new UpdateHandler(),
}.ToFrozenDictionary();
// DON'T: Return mutable collection
public List<Order> GetOrders()
{
return _orders; // Caller can modify!
}
快速参考
| 模式 | 优点 |
|---|---|
sealed class | 去虚拟化,API 清晰 |
readonly record struct | 无防御性复制,具有值语义 |
| 静态纯函数 | 无虚方法表,易于测试,线程安全 |
延迟调用 .ToList() | 仅物化一次 |
在热点路径使用 ValueTask | 避免分配 Task |
对字节使用 Span<T> | 栈分配,无需复制 |
返回 IReadOnlyList<T> | 不可变 API 契约 |
FrozenDictionary | 静态数据的最快查找方式 |
反模式
// DON'T: Unsealed class without reason
public class OrderService { } // Seal it!
// DON'T: Mutable struct
public struct Point { public int X; public int Y; } // Make readonly
// DON'T: Instance method that could be static
public int Add(int a, int b) => a + b; // Make static
// DON'T: Multiple ToList() calls
items.Where(...).ToList().OrderBy(...).ToList(); // One ToList at end
// DON'T: Return List<T> from public API
public List<Order> GetOrders(); // Return IReadOnlyList<T>
// DON'T: ValueTask for always-async operations
public ValueTask<Order> CreateOrderAsync(); // Just use Task
// DON'T: Use `Span<T>` in async methods
public async Task ProcessAsync(Span<byte> data); // Use Memory<T>
// DON'T: Use `FrozenDictionary` for mutable data
// It has no add/remove APIs
Agent 注意事项
- 不要默认所有类型都使用
class—— 请评估结构体与类的选择矩阵。 - 不要创建非只读结构体 —— 可变结构体会导致难以察觉的错误。
- 不要在异步方法中使用
Span<T>—— 异步代码应使用Memory<T>。 - 不要将
FrozenDictionary用于可变数据 —— 它没有添加或删除 API。 - 不要密封抽象类或专为扩展设计的类。
- 未经测量,不要创建大型结构体(> 64 bytes) —— 请对复制成本进行基准测试。
- 不要在高频路径中使用
Dictionary<K,V>作为静态查找表 —— 请使用FrozenDictionary。 - 大型只读结构体参数别忘了加
in—— 不加in会复制结构体。
资源
- 性能最佳实践:https://learn.microsoft.com/en-us/dotnet/standard/performance/
- Span 指南:https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/
- 冻结集合:https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen
- 框架设计指南:类型设计:https://learn.microsoft.com/dotnet/standard/design-guidelines/type
- 在 class 和 struct 之间进行选择:https://learn.microsoft.com/dotnet/standard/design-guidelines/choosing-between-class-and-struct
- ref struct 类型:https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/ref-struct
- 记录(C# 参考):https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/record
评论
支持 Markdown