mcpField
T
type-design-performance

type-design-performance

github.com/wshaddix开发8670 次安装36 周前更新
代码仓库

关于 type-design-performance

面向 .NET 开发者的类型设计技能,提供围绕性能、内存分配与 API 形状的设计和审查指导。内容涵盖结构体、集合、枚举及异步数据处理等选择。

主要功能

  1. 01结构体与类的选择建议
  2. 02默认密封类型的指导
  3. 03只读结构体设计要点
  4. 04集合类型选择建议
  5. 05延迟枚举与异步流实践
  6. 06Span、Memory 与 ValueTask 用法

适用场景

  1. 01设计新的类型和 API
  2. 02审查代码中的性能问题
  3. 03选择 class、struct 或 record
  4. 04为集合及可枚举数据选择模式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill type-design-performance

安装后在 Claude Code 中输入/type-design-performance确认已加载。

详细介绍

面向性能的类型设计

何时使用此技能

在以下情况下使用此技能:

  • 设计新类型和 API
  • 检查代码中的性能问题
  • 在 class、struct 和 record 之间进行选择
  • 使用集合和可枚举对象

核心原则

  1. 密封类型——除非明确设计为可继承
  2. 优先使用只读结构体——适用于小型、不可变的值类型
  3. 优先使用静态纯函数——性能和可测试性更好
  4. 延迟枚举——在需要之前不要物化
  5. 返回不可变集合——从 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生命周期长,在中间件间共享

默认密封

为什么要密封库类型

对于库类型(供其他程序集使用的代码),默认应密封类:

  1. JIT 去虚拟化——密封类允许 JIT 将虚调用替换为直接调用,从而支持内联
  2. 契约更简单——未密封类意味着承诺支持继承
  3. 减少破坏性更改——之后再密封类会造成二进制兼容性破坏
// 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 classrecord 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 注意事项

  1. 不要默认所有类型都使用 class —— 请评估结构体与类的选择矩阵。
  2. 不要创建非只读结构体 —— 可变结构体会导致难以察觉的错误。
  3. 不要在异步方法中使用 Span<T> —— 异步代码应使用 Memory<T>。
  4. 不要将 FrozenDictionary 用于可变数据 —— 它没有添加或删除 API。
  5. 不要密封抽象类或专为扩展设计的类。
  6. 未经测量,不要创建大型结构体(> 64 bytes) —— 请对复制成本进行基准测试。
  7. 不要在高频路径中使用 Dictionary<K,V> 作为静态查找表 —— 请使用 FrozenDictionary。
  8. 大型只读结构体参数别忘了加 in —— 不加 in 会复制结构体。

资源

评论

支持 Markdown