.NET 9(2024.11)· 实现接口 · allows ref struct · 进入 async 方法
C# 7.2 引入 ref struct 以来(回顾 L16),三个困扰开发者的问题始终存在:
Span<T> 无法参与 IEnumerable<T>、IDisposable 等泛型抽象C# 13 一次性解决了这三个问题。三个特性共享同一个设计动机:让 ref struct 不再是一等公民之外的二等类型。
// C# 12:同样的逻辑要写三份重载——只因为 Span 不实现接口
void Process(int[] data) => ProcessCore(data);
void Process(List<int> data) => ProcessCore(data);
void Process(Span<int> data) => ProcessCore(data); // 重载 × 3
// 理想:对实现了 IEnumerable<int> 的类型只写一份
// 现实:Span<int> 不实现任何接口 → 做不到
// 自定义 ref struct 实现 IDisposable
ref struct Buffer : IDisposable {
public Span<byte> Data;
public void Dispose() => Data.Clear();
}
// 可以用 using 语句了!
using (var buf = new Buffer { Data = stackalloc byte[32] }) {
// ... 使用 buf ...
} // 自动调用 Dispose()——零分配
// .NET 9 BCL:Span<T> / ReadOnlySpan<T> 已实现 IEnumerable<T> 等接口
IDisposable d = myRefStruct; 编译错误(装箱会让 ref struct 逃逸到堆)this 调用会触发装箱)
// 泛型方法——通过约束来消费接口
void UseBuffer<T>(T buffer) where T : IDisposable {
using (buffer) { /* ... */ }
}
Buffer buf = new(); // ref struct
UseBuffer(buf); // ✅ C# 13——泛型在调用点被特化,不装箱
你很少需要自己写 ref struct 实现接口。但整个 BCL 已经为 Span 做了这件事——你直接受益。
即使 ref struct 实现了接口,泛型参数仍然不接受它——因为编译器要确保 T 可以装箱/堆分配:
// C# 12:即使 Span 实现了 IEnumerable<int>,这里还是传不进去
void ForEach<T>(T collection) where T : IEnumerable<int> { ... }
ForEach(new[] { 1, 2, 3 }); // ✅ int[]
ForEach(new List<int> { 1, 2 }); // ✅ List<int>
// ForEach((ReadOnlySpan<int>)(...)); // ❌ Span 不能作为 T
allows ref struct 不是限制 T 必须是什么,而是额外允许 T 是 ref struct——这是 C# 历史上第一个"反约束":
// 加一行 allows ref struct
void ForEach<T>(T collection)
where T : IEnumerable<int>
where T : allows ref struct // ← 反约束:扩张——放行 ref struct
{
foreach (var item in collection)
Console.WriteLine(item);
}
// 全都可以传!
ForEach(new[] { 1, 2, 3 });
ForEach(new List<int> { 1, 2 });
ForEach((ReadOnlySpan<int>)stackalloc int[] { 1, 2 }); // ✅ C# 13
where T : class)是收缩——只允许某类类型。反约束(allows ref struct)是扩张——在原有约束基础上多放行一种类型。两者可以叠加:where T : IEnumerable<int>, allows ref struct。
标记了 allows ref struct 的泛型方法,编译器在调用点对 ref struct 做特化(specialization)——不装箱、不逃逸。和其他类型走同一套泛型代码路径,但 IL 层面有专门处理。
// 反约束与普通约束可以组合
void Work<T>(T item)
where T : struct // 必须是值类型
where T : allows ref struct // 额外放行 ref struct
{ }
// 常见组合:
// where T : IDisposable, allows ref struct
// where T : IEnumerable<TElement>, allows ref struct
C# 12:async 方法和迭代器(yield return)中完全禁止使用 ref 局部变量和 unsafe 块。哪怕你在 await 之前用完就扔了也不行。
// C# 12:编译错误 CS8344
async Task<int> ReadHeaderAsync(Stream stream) {
Span<byte> buffer = stackalloc byte[16]; // ❌ async 方法不能有 ref struct
await stream.ReadAsync(buffer);
return BitConverter.ToInt32(buffer);
}
这是最令人沮丧的限制之一:你知道 buffer 不跨越 await——你只是想在读 I/O 之前临时用一下栈上的 buffer——但编译器不让你这么做。
规则很简单——ref 变量不能跨越 await / yield 边界。必须在 await 之前"死亡"(最后一次引用之后),await 之后不能再访问:
async Task<int> ReadAsync(Stream s) {
Span<byte> buf = stackalloc byte[16];
// ❌ CS8344
}
async Task<int> ReadAsync(Stream s) {
Span<byte> buf = stackalloc byte[16];
s.Read(buf); // 同步读取——没有 await
var r = BitConverter.ToInt32(buf); // buf 在这里"死亡"
// 之后不再引用 buf——安全
await s.FlushAsync(); // ✅ await 在 buf 死亡之后
return r;
}
// ❌ 但仍然不能跨越 await——编译器会在 await 后将 ref 变量标记为"未赋值"
async Task Bad(Stream s) {
Span<byte> buf = stackalloc byte[16];
await Task.Delay(1);
Console.WriteLine(buf[0]); // ❌ CS4015: ref local 在 await 后已失效
}
| 场景 | 允许 | 不允许 |
|---|---|---|
Span<T> 在 await 之前 | ✅ | |
Span<T> 跨越 await | ❌ 编译器视为已失效 | |
unsafe { } 含 await | ❌ await 不能在 unsafe 中 | |
unsafe { } 在迭代器中 | ✅(不含 yield 的部分) | ❌ yield return 不能在 unsafe 中 |
byte[](堆分配),现在可以用 stackalloc(零分配)。核心思想:同步解析部分用 Span(零分配栈内存),跨越 await 时才用堆对象。
三个特性对普通开发者的影响程度不同:
| 特性 | 你直接写吗 | 好处怎么到你身上 |
|---|---|---|
| ref struct 实现接口 | 几乎不 | BCL 替你做完了——Span<T> 现在实现 IEnumerable<T>,你能直接把 Span 传给 string.Join、foreach 等 |
| allows ref struct | 偶尔 | 你写的泛型工具方法加一行约束,就能接受 Span 参数。不写泛型方法就用不到 |
| ref 进 async | 经常 | 任何 async I/O 方法里,想用 stackalloc 替代 new byte[] 省一次堆分配,直接写就行 |
真正每天受益的是第三个。比如之前你要在 async 方法里读 header:
async Task<int> ReadHeaderAsync(Stream s) {
byte[] buf = new byte[16]; // 堆分配——只为了 16 字节
await s.ReadAsync(buf, 0, 16);
return BitConverter.ToInt32(buf, 0);
}
async Task<int> ReadHeaderAsync(Stream s) {
Span<byte> buf = stackalloc byte[16]; // 零分配
s.Read(buf); // 同步读完
var result = BitConverter.ToInt32(buf); // buf 死亡
await s.FlushAsync(); // 可以 await
return result;
}
| 特性 | 解决的问题 | 使用场景 |
|---|---|---|
| ref struct 实现接口 | Span 不能参与泛型抽象 | 消除 Span 的冗余重载;泛型约束消费 |
| allows ref struct | 泛型天然拒绝 ref struct | 让泛型方法接受 Span 等 ref struct 参数 |
| ref/unsafe 进 async | async 方法完全不能用 Span | I/O 场景——await 前用 stackalloc 零分配 |
\e 转义、方法组自然类型改进、partial 属性、OverloadResolutionPriority 等。IEnumerable<T> 的泛型方法——给它们加上 allows ref struct,然后试试传 Span。