dotnet-csharp-type-design-performance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesedotnet-csharp-type-design-performance
.NET C#类型设计与性能优化
Upfront type design choices that affect performance throughout an application's lifetime. Covers the struct vs class decision matrix, sealed by default for library types, readonly struct for defensive copy elimination, ref struct and Span<T>/Memory<T> selection, and collection type selection including FrozenDictionary. This skill focuses on designing types correctly from the start, not on optimizing existing code.
Version assumptions: .NET 8.0+ baseline. FrozenDictionary (requires .NET 8+) is in-scope by default.
提前做好的类型设计选择会影响应用程序整个生命周期的性能。内容涵盖struct与class的决策矩阵、库类型默认密封、使用readonly struct消除防御性复制、ref struct与Span<T>/Memory<T>的选择,以及包括FrozenDictionary在内的集合类型选择。本技能专注于从一开始就正确设计类型,而非优化现有代码。
版本假设: 以.NET 8.0+为基准。默认包含FrozenDictionary(需要.NET 8+)。
Scope
适用范围
- struct vs class decision matrix
- sealed by default for library types
- readonly struct for defensive copy elimination
- ref struct and Span<T>/Memory<T> selection
- Collection type selection (FrozenDictionary, ImmutableArray)
- struct与class的决策矩阵
- 库类型默认密封
- 使用readonly struct消除防御性复制
- ref struct与Span<T>/Memory<T>的选择
- 集合类型选择(FrozenDictionary、ImmutableArray)
Out of scope
不适用范围
- Runtime optimization techniques (pooling, caching, stackalloc) -- see [skill:dotnet-performance-patterns]
- Language syntax for records and collection expressions -- see [skill:dotnet-csharp-modern-patterns]
- GC behavior and memory management -- see [skill:dotnet-gc-memory]
Cross-references: [skill:dotnet-performance-patterns] for optimization techniques, [skill:dotnet-csharp-modern-patterns] for language syntax, [skill:dotnet-gc-memory] for GC behavior and memory management.
- 运行时优化技术(对象池、缓存、stackalloc)——详见[skill:dotnet-performance-patterns]
- 记录类型与集合表达式的语法——详见[skill:dotnet-csharp-modern-patterns]
- GC行为与内存管理——详见[skill:dotnet-gc-memory]
交叉引用:优化技术请参考[skill:dotnet-performance-patterns],语法请参考[skill:dotnet-csharp-modern-patterns],GC行为与内存管理请参考[skill:dotnet-gc-memory]。
Struct vs Class Decision Matrix
Struct与Class决策矩阵
Choosing between and at design time has cascading effects on allocation, GC pressure, copying cost, and API shape. Make the decision once, correctly.
structclass在设计阶段选择还是会对内存分配、GC压力、复制成本和API形态产生连锁影响。请一次性做出正确的选择。
structclassDecision Criteria
判定标准
| Criterion | Favors | Favors |
|---|---|---|
| Size | Small (<= 16 bytes ideal, <= 64 bytes acceptable) | Large or variable size |
| Lifetime | Short-lived, method-scoped | Long-lived, shared across scopes |
| Identity | Value equality (two instances with same data are equal) | Reference identity matters |
| Mutability | Immutable ( | Mutable or complex state transitions |
| Inheritance | Not needed | Requires polymorphism or base class |
| Nullable semantics | | Needs explicit null to signal absence |
| Collection usage | Stored in arrays/spans (contiguous memory, cache-friendly) | Stored via references (indirection on every access) |
| 判定标准 | 倾向于 | 倾向于 |
|---|---|---|
| 大小 | 较小(理想<=16字节,可接受<=64字节) | 较大或可变大小 |
| 生命周期 | 短生命周期,方法作用域内 | 长生命周期,跨作用域共享 |
| 标识 | 值相等(数据相同的两个实例相等) | 引用标识重要 |
| 可变性 | 不可变( | 可变或有复杂状态转换 |
| 继承 | 不需要 | 需要多态或基类 |
| 可空语义 | | 需要显式null表示缺失 |
| 集合使用 | 存储在数组/span中(连续内存,缓存友好) | 通过引用存储(每次访问都有间接开销) |
Size Guidelines
大小指南
<= 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 avoidanceThe 16-byte threshold comes from x64 calling conventions: two register-sized values can be passed in registers without stack spilling. Beyond that, the struct is passed by reference on the stack, and copying becomes the dominant cost.
<= 16字节: 理想的struct——可放入两个寄存器,传递高效
17-64字节: 可接受的struct——需衡量复制成本与分配成本
> 64字节: 优先选择class——复制成本超过了避免分配的收益16字节阈值来自x64调用约定:两个寄存器大小的值可以直接在寄存器中传递,无需栈溢出。超过该值后,struct会在栈上按引用传递,复制成为主要成本。
Common Types and Their Correct Design
常见类型及其正确设计
| Type | Correct Choice | Why |
|---|---|---|
| Point2D (8 bytes: two floats) | | Small, immutable, value semantics |
| Money (16 bytes: decimal + currency enum) | | Small, immutable, value equality |
| DateRange (16 bytes: two DateOnly) | | Small, immutable, value semantics |
| Matrix4x4 (64 bytes: 16 floats) | | Performance-critical math, contiguous arrays |
| CustomerDto (variable: strings, lists) | | Contains references, variable size |
| HttpRequest context | | Long-lived, shared across middleware |
| 类型 | 正确选择 | 原因 |
|---|---|---|
| Point2D(8字节:两个float) | | 体积小、不可变、值语义 |
| Money(16字节:decimal + 货币枚举) | | 体积小、不可变、值相等 |
| DateRange(16字节:两个DateOnly) | | 体积小、不可变、值语义 |
| Matrix4x4(64字节:16个float) | | 性能关键的数学运算,连续数组 |
| CustomerDto(可变大小:字符串、列表) | | 包含引用,大小可变 |
| HttpRequest上下文 | | 长生命周期,跨中间件共享 |
Sealed by Default
默认密封类
Why Seal Library Types
为何要密封库类型
For library types (code consumed by other assemblies), seal classes by default:
- JIT devirtualization -- sealed classes enable the JIT to replace virtual calls with direct calls, enabling inlining. See [skill:dotnet-performance-patterns] for benchmarking this effect.
- Simpler contracts -- unsealed classes imply a promise to support inheritance, which constrains future changes.
- Fewer breaking changes -- sealing a class later is a binary-breaking change. Starting sealed and unsealing later is safe.
csharp
// 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);
// Template method pattern -- intentional extension point
protected virtual void OnValidationComplete(Widget widget) { }
}对于库类型(供其他程序集使用的代码),默认密封类:
- JIT去虚拟化——密封类使JIT能够将虚调用替换为直接调用,从而支持内联。有关基准测试请参考[skill:dotnet-performance-patterns]。
- 更简洁的契约——未密封的类意味着承诺支持继承,这会限制未来的变更。
- 更少的破坏性变更——后续密封类是二进制破坏性变更。从密封开始,之后再取消密封是安全的。
csharp
// 良好实践——库类型默认密封
public sealed class WidgetService
{
public Widget GetWidget(int id) => new(id, "Default");
}
// 仅当继承是有意的设计决策时才取消密封
public abstract class WidgetValidatorBase
{
public abstract bool Validate(Widget widget);
// 模板方法模式——有意设计的扩展点
protected virtual void OnValidationComplete(Widget widget) { }
}When NOT to Seal
何时不密封
| Scenario | Reason |
|---|---|
| Abstract base classes | Inheritance is the purpose |
| Framework extensibility points | Consumers need to subclass |
| Test doubles in non-mockable designs | Mocking frameworks need to subclass (prefer interfaces) |
| Application-internal classes with no consumers | Sealing adds no value (no external callers to devirtualize) |
| 场景 | 原因 |
|---|---|
| 抽象基类 | 继承是其设计目的 |
| 框架扩展点 | 使用者需要子类化 |
| 不可模拟设计中的测试替身 | 模拟框架需要子类化(优先使用接口) |
| 无外部使用者的应用内部类 | 密封无价值(无外部调用者可去虚拟化) |
readonly struct
readonly struct
Mark structs when all fields are immutable. This eliminates defensive copies the JIT creates when accessing structs through parameters or fields.
readonlyinreadonly当所有字段都是不可变的时,将struct标记为。这可以消除JIT在通过参数或字段访问struct时创建的防御性复制。
readonlyinreadonlyThe Defensive Copy Problem
防御性复制问题
csharp
// 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);
}
// In a readonly context, JIT copies the struct before calling Length()
// because Length() MIGHT mutate X or Y
public double GetLength(in MutablePoint point)
{
return point.Length(); // Hidden copy here!
}csharp
// 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);
}
// No defensive copy -- JIT can call Length() directly on the reference
public double GetLength(in ImmutablePoint point)
{
return point.Length(); // No copy, direct call
}csharp
// 非readonly struct——JIT在每次方法调用时必须进行防御性复制
public struct MutablePoint
{
public double X;
public double Y;
public double Length() => Math.Sqrt(X * X + Y * Y);
}
// 在readonly上下文中,JIT会在调用Length()前复制struct
// 因为Length()可能会修改X或Y
public double GetLength(in MutablePoint point)
{
return point.Length(); // 此处存在隐藏的复制!
}csharp
// 良好实践——readonly struct:JIT知道不会有状态变更
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);
}
// 无防御性复制——JIT可以直接对引用调用Length()
public double GetLength(in ImmutablePoint point)
{
return point.Length(); // 无复制,直接调用
}readonly struct Checklist
readonly struct检查清单
- All fields are or
readonly/{ get; }properties{ get; init; } - No methods mutate state
- Constructor initializes all fields
- Consider for value comparison without boxing
IEquatable<T>
- 所有字段都是或
readonly/{ get; }属性{ get; init; } - 没有方法会修改状态
- 构造函数初始化所有字段
- 考虑实现以避免装箱的情况下进行值比较
IEquatable<T>
ref struct and Span/Memory Selection
ref struct与Span/Memory选择
ref struct Constraints
ref struct的限制
ref structSpan<T>ref structSpan<T>Span<T> vs Memory<T> Decision
Span<T> vs Memory<T>决策
| Criterion | Use | Use |
|---|---|---|
| Synchronous method | Yes | Yes (but Span is lower overhead) |
| Async method | No (ref struct) | Yes |
| Store in field/collection | No (ref struct) | Yes |
| Pass to callback/delegate | No | Yes |
| Slice without allocation | Yes | Yes |
| Wrap stackalloc buffer | Yes | No |
| 判定标准 | 使用 | 使用 |
|---|---|---|
| 同步方法 | 是 | 是(但Span的开销更低) |
| 异步方法 | 否(ref struct限制) | 是 |
| 存储在字段/集合中 | 否(ref struct限制) | 是 |
| 传递给回调/委托 | 否 | 是 |
| 无分配切片 | 是 | 是 |
| 包装stackalloc缓冲区 | 是 | 否 |
Selection Flowchart
选择流程图
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; Memory<T> is also acceptable是否会在异步方法中使用缓冲区或存储在字段中?
是 -> 使用Memory<T>(同步处理时通过.Span转换为Span<T>)
否 -> 是否需要包装stackalloc缓冲区?
是 -> 使用Span<T>
否 -> 优先选择Span<T>以获得最低开销;Memory<T>也是可接受的Practical Pattern
实用模式
csharp
// Public API uses Memory<T> for maximum flexibility
public async Task<int> ProcessAsync(ReadOnlyMemory<byte> data,
CancellationToken ct = default)
{
// Hand off to awaitable I/O
await _stream.WriteAsync(data, ct);
// Convert to Span for synchronous processing
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;
}csharp
// 公共API使用Memory<T>以获得最大灵活性
public async Task<int> ProcessAsync(ReadOnlyMemory<byte> data,
CancellationToken ct = default)
{
// 交给可等待的I/O操作
await _stream.WriteAsync(data, ct);
// 转换为Span进行同步处理
return CountNonZero(data.Span);
}
// 内部热点路径方法使用Span<T>以实现零开销
private static int CountNonZero(ReadOnlySpan<byte> data)
{
var count = 0;
foreach (var b in data)
{
if (b != 0) count++;
}
return count;
}Collection Type Selection
集合类型选择
Decision Matrix
决策矩阵
| Scenario | Recommended Type | Rationale |
|---|---|---|
| Build once, read many | | Optimized read layout, immutable after creation (.NET 8+) |
| Build once, read many (pre-.NET 8) | | Thread-safe, immutable |
| Concurrent read/write | | Thread-safe without external locking |
| Frequent modifications | | Lowest per-operation overhead for single-threaded access |
| Ordered data | | O(log n) lookup with sorted enumeration |
| Return from public API | | Immutable interface communicates intent |
| Stack-allocated small collection | | Zero GC pressure for small, known-size buffers |
| 场景 | 推荐类型 | 理由 |
|---|---|---|
| 一次构建、多次读取 | | 优化的读取布局,创建后不可变(.NET 8+) |
| 一次构建、多次读取(.NET 8之前) | | 线程安全、不可变 |
| 并发读写 | | 线程安全,无需外部锁 |
| 频繁修改 | | 单线程访问时每次操作开销最低 |
| 有序数据 | | O(log n)查找,支持有序枚举 |
| 公共API返回 | | 不可变接口传达设计意图 |
| 栈分配小型集合 | 使用stackalloc的 | 零GC压力,适用于已知大小的小型缓冲区 |
FrozenDictionary (.NET 8+)
FrozenDictionary(.NET 8+)
FrozenDictionary<K,V>Dictionarycsharp
using System.Collections.Frozen;
// One-time creation cost during initialization
private static readonly FrozenDictionary<string, int> StatusCodes =
new Dictionary<string, int>
{
["OK"] = 200,
["NotFound"] = 404,
["InternalServerError"] = 500
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
// Optimized lookups on every request
public int GetStatusCode(string name) =>
StatusCodes.TryGetValue(name, out var code) ? code : -1;When to use FrozenDictionary:
- Configuration lookup tables populated at startup
- Static mappings (enum-to-string, error codes, feature flags)
- Any dictionary that is populated once and read many times
When NOT to use FrozenDictionary:
- Data that changes at runtime (use or
Dictionary)ConcurrentDictionary - Small lookups (< 10 items) where the optimization overhead is not recouped
FrozenDictionary<K,V>Dictionarycsharp
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;何时使用FrozenDictionary:
- 启动时填充的配置查找表
- 静态映射(枚举转字符串、错误码、功能标志)
- 任何一次构建、多次读取的字典
何时不使用FrozenDictionary:
- 运行时会变更的数据(使用或
Dictionary)ConcurrentDictionary - 小型查找(<10个条目),优化开销无法抵消收益
ImmutableDictionary vs FrozenDictionary
ImmutableDictionary vs FrozenDictionary
| Characteristic | | |
|---|---|---|
| Mutability | Immutable (returns new instance on "Add") | Immutable (no add/remove APIs) |
| Read performance | Good | Excellent (optimized layout) |
| Creation | Fast (incremental building) | Slower (one-time optimization) |
| Evolution | Supports Add/Remove (new instance) | No mutation -- rebuild from scratch |
| Thread safety | Inherently thread-safe | Inherently thread-safe |
| 特性 | | |
|---|---|---|
| 可变性 | 不可变("添加"时返回新实例) | 不可变(无添加/移除API) |
| 读取性能 | 良好 | 极佳(优化布局) |
| 创建速度 | 快(增量构建) | 慢(一次性优化) |
| 扩展能力 | 支持添加/移除(返回新实例) | 无变更能力——需从头重建 |
| 线程安全 | 天生线程安全 | 天生线程安全 |
Record Types for Data Transfer
用于数据传输的记录类型
record class vs record struct
record class vs record struct
| Characteristic | | |
|---|---|---|
| Allocation | Heap | Stack (or inline in arrays) |
| Equality | Reference type with value equality | Value type with value equality |
| Creates new heap object | Creates new stack copy |
| Nullable | | |
| Size | Reference (8 bytes on x64) + heap | Full size on stack |
csharp
// record class -- heap allocated, good for DTOs passed through layers
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);
// Use readonly record struct when:
// 1. Size <= ~64 bytes
// 2. Value semantics are desired
// 3. High-throughput scenarios where allocation matters| 特性 | | |
|---|---|---|
| 内存分配 | 堆 | 栈(或内联在数组中) |
| 相等性 | 引用类型,值相等语义 | 值类型,值相等语义 |
| 创建新的堆对象 | 创建新的栈副本 |
| 可空性 | | |
| 大小 | 引用(x64上8字节)+ 堆 | 栈上的完整大小 |
csharp
// record class——堆分配,适用于跨层传递的DTO
public record CustomerDto(string Name, string Email, DateOnly JoinDate);
// readonly record struct——栈分配,适用于小型值对象
public readonly record struct Money(decimal Amount, string Currency);
// 在以下情况使用readonly record struct:
// 1. 大小<=约64字节
// 2. 需要值语义
// 3. 分配影响显著的高吞吐量场景Agent Gotchas
注意事项
- Do not default to for every type -- evaluate the struct vs class decision matrix. Small, immutable value objects (coordinates, money, date ranges) should be
classto avoid unnecessary heap allocations.readonly struct - Do not create non-readonly structs -- mutable structs cause subtle bugs (defensive copies, lost mutations on copy). If a struct needs mutation, reconsider whether it should be a class. Always mark structs .
readonly - Do not use in async methods --
Span<T>is aSpan<T>and cannot crossref structboundaries. Useawaitfor async code and convert toMemory<T>viaSpan<T>for synchronous processing sections..Span - Do not use for mutable data -- it has no add/remove APIs. It is designed for build-once-read-many scenarios. Use
FrozenDictionaryorDictionary<K,V>for data that changes at runtime.ConcurrentDictionary<K,V> - Do not seal abstract classes or classes designed as extension points -- sealing is a design-time decision for concrete types. Abstract classes and intentional base classes must remain unsealed.
- Do not make large structs (> 64 bytes) without measuring -- large structs are expensive to copy. If passed by value (no modifier), they may be slower than a heap-allocated class. Benchmark with [skill:dotnet-performance-patterns].
in - Do not use for static lookup tables in hot paths -- if the dictionary is populated at startup and never modified, use
Dictionary<K,V>for optimized read performance. Requires .NET 8+.FrozenDictionary - Do not forget parameter for large readonly structs -- without
in, the struct is copied on every method call. Withinon ain, the JIT passes by reference with no defensive copy.readonly struct
- 不要默认对所有类型使用——请评估struct与class的决策矩阵。小型、不可变的值对象(坐标、金额、日期范围)应使用
class,以避免不必要的堆分配。readonly struct - 不要创建非readonly的struct——可变struct会导致微妙的bug(防御性复制、复制时丢失变更)。如果struct需要变更,请重新考虑是否应该是class。始终将struct标记为。
readonly - 不要在异步方法中使用——
Span<T>是Span<T>,无法跨越ref struct边界。异步代码使用await,同步处理部分通过Memory<T>转换为.Span。Span<T> - 不要对可变数据使用——它没有添加/移除API。它专为一次构建、多次读取的场景设计。运行时变更的数据请使用
FrozenDictionary或Dictionary<K,V>。ConcurrentDictionary<K,V> - 不要密封抽象类或设计为扩展点的类——密封是针对具体类型的设计决策。抽象类和有意设计的基类必须保持未密封。
- 不要未经测试就创建大型struct(>64字节)——大型struct复制成本高。如果按值传递(无修饰符),可能比堆分配的class更慢。请使用[skill:dotnet-performance-patterns]进行基准测试。
in - 不要在热点路径中对静态查找表使用——如果字典在启动时填充且永不修改,请使用
Dictionary<K,V>以获得优化的读取性能。需要.NET 8+。FrozenDictionary - 不要忘记对大型readonly struct使用参数——没有
in的话,每次方法调用都会复制struct。对in使用readonly struct,JIT会按引用传递且无防御性复制。in
Prerequisites
前置条件
- .NET 8.0+ SDK (required for ,
FrozenDictionary)FrozenSet - Understanding of GC generations and heap behavior (see [skill:dotnet-gc-memory])
- Familiarity with performance measurement (see [skill:dotnet-performance-patterns])
- namespace (.NET 8+)
System.Collections.Frozen - namespace
System.Collections.Immutable
- .NET 8.0+ SDK(、
FrozenDictionary所需)FrozenSet - 了解GC代与堆行为(详见[skill:dotnet-gc-memory])
- 熟悉性能测量(详见[skill:dotnet-performance-patterns])
- 命名空间(.NET 8+)
System.Collections.Frozen - 命名空间
System.Collections.Immutable