补全参数传递的语义三角 + 安全代码里的固定大小缓冲区
在 C# 12 之前,引用传递只有两个选项:ref(可写引用)和 in(只读引用,可能复制)。中间缺了一格——"我需要原始内存位置,但不会修改它"。
ref可写引用。必须传变量。方法可以修改值。调用方必须写 ref。
Swap、TryGet、Interlocked
ref readonly C# 12只读引用,保证指向原地址。必须传变量。方法不能修改值。调用方必须写 ref。
互操作、大 struct 只读检查、IsNullRef
in只读引用,编译器可能复制到临时变量。可传字面量/表达式。调用方可省略 in。
大 struct 传参优化
ref | ref readonly 新 | in | |
|---|---|---|---|
| 必须传变量 | ✅ | ✅ | ❌ 值/表达式均可 |
| 方法可修改 | ✅ | ❌ | ❌ |
| 保证指向原地址 | ✅ | ✅ | ❌ 编译器可能复制 |
| 调用方写修饰符 | 必须写 ref | 必须写 ref | 可省略 |
// 场景 1:需要内存地址的方法——in 不能保证
bool IsNull<T>(ref readonly T value) => Unsafe.IsNullRef(ref value);
// ↑ 如果 value 是 in,编译器可能复制到一个临时变量——IsNullRef 就判断错了
// 场景 2:更新旧 API——ref 改成 ref readonly 不是 Breaking Change
// 旧:void Process(ref LargeStruct data) { /* 只读 */ }
// 新:void Process(ref readonly LargeStruct data) { /* 只读 */ }
// 调用方已经写了 ref,行为不变——但现在编译器会阻止方法内意外修改
// 场景 3:in 能传字面量——但你不需要这个灵活性
M(in 42); // ✅——但 42 被复制到临时变量,不是原始位置
M(ref readonly 42); // ❌ 编译错误——字面量没有地址
C# 的 struct 是值类型,但其实例方法可以修改自身。编译器看到 in 参数时面临一个矛盾:
in 承诺"方法不能改参数"编译器的保守策略:先复制一份,在副本上操作。这就是防御性拷贝Defensive Copy。
| 场景 | struct 类型 | 操作 | 防御性拷贝? |
|---|---|---|---|
| 直接字段访问 | 可变 | p.X(字段) | ❌ 不复制 |
| 调属性 getter(未标 readonly) | 可变 | p.X(属性) | ✅ 复制 |
| 调实例方法(未标 readonly) | 可变 | p.Offset() | ✅ 复制 |
调 readonly 成员 | 可变 | p.Distance | ❌ 不复制 |
| 任意操作 | readonly struct | 任何成员 | ❌ 不复制 |
| 传字面量/表达式 | 任意 | M(in 42) | ✅ 复制 |
| 把 in 传给另一个 in | 任意 | 引用透传 | ❌ 不复制 |
struct Point { public int X, Y; public int Sum => X + Y; } // 可变 struct
void Example(in Point p)
{
var a = p.X; // ❌ 不复制——直接字段访问,编译器确定安全
var b = p.Sum; // ⚡ 复制!Sum 是 getter(属性),编译器不信任
}
// ———————————————————
readonly struct ReadOnlyPoint { public int X { get; } public int Y { get; } }
void Example2(in ReadOnlyPoint p)
{
var a = p.X; // ❌ 不复制——readonly struct 不会变,编译器完全信任
}
// ———————————————————
struct Mixed // 可变 struct,但个别成员标了 readonly
{
public int X { get => field; set => field = value; }
public readonly double Length => Math.Sqrt(X * X); // 承诺不修改
}
void Example3(in Mixed m)
{
var a = m.X; // ⚡ 复制!getter 没标 readonly
var b = m.Length; // ❌ 不复制——Length 标了 readonly
}
// C#: void Bad(in Point p) { Console.WriteLine(p.X); } ← X 是属性 getter
// 生成的 IL:
// ldarg.1 加载 in 参数的地址
// ldobj Point 从地址复制整个 struct 到栈 ← 防御性拷贝
// stloc.0 存入临时变量
// ldloca.s 0 加载临时变量的地址
// call get_X() 在临时变量上调用 getter
in 传递可变 struct 可能每次成员访问都复制一次。循环中每次迭代都防御性拷贝 = 性能反而更差。
readonly struct(最彻底)readonly(C# 8.0+,逐个击破)完整速查表见 in 防御性拷贝参考文档——含决策树、IL 对比、最佳实践。
ref——不是 ref readonly这是最反直觉的点:
void Print(ref readonly int x) => Console.WriteLine(x);
var v = 42;
Print(ref v); // ✅ 调用方写 ref
Print(ref readonly v); // ❌ 编译错误——调用方没有 ref readonly 语法
Print(v); // ❌ 编译错误——必须写 ref
Print(ref 42); // ⚠ 编译警告——字面量创建临时变量,ref 失去意义
ref readonly 是被调用方的承诺("我承诺不修改"),不是调用方要关心的新语法。调用方看到 ref 就知道"我在传引用"——这已经是最大的信息量了。方法是否修改那是方法的事。这和 in 的设计哲学不同——in 对调用方完全透明(可省略),因为它承诺的是"我不在乎是不是引用"。
以前在 struct 里嵌入固定大小数组,只能用 unsafe fixed:
// ❌ 旧世界:unsafe 是硬门槛
unsafe struct Buffer
{
public fixed int items[10]; // 只能在 unsafe 上下文中使用
}
// 使用也必须 unsafe
unsafe { var x = buffer.items[0]; }
[InlineArray] 属性一个属性 + 一个 struct + 一个字段 = 固定大小的安全数组:
// ✅ C# 12:纯 safe code
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10
{
private int _element0; // "哨兵字段"——编译器据此推断元素类型和布局
}
// 使用:和普通数组完全一样
var buf = new Buffer10();
for (int i = 0; i < 10; i++) buf[i] = i; // 索引器
foreach (var v in buf) Console.WriteLine(v); // foreach
// 关键:零开销转 Span
Span<int> span = buf; // 隐式转换——struct 直接在内存中就是 Span 指向的区域
// 你写:
[InlineArray(3)]
public struct Triple { private int _e0; }
// 编译器生成:
// 1. struct Triple 的大小 = 3 × sizeof(int) = 12 字节
// 2. 索引器:public ref int this[int index] { get { ... } }
// 3. GetEnumerator() → 支持 foreach
// 4. 隐式转换 → Span<int> / ReadOnlySpan<int>
//
// 本质上 = 把 unsafe fixed size buffer 包装成安全特性
stackalloc | 内联数组 [InlineArray] | |
|---|---|---|
| 安全上下文 | Span 接收可免 unsafe;否则需要 | ✅ 纯 safe code |
| 存储位置 | 只能在栈上(局部变量) | 可以做 struct 字段——嵌入任何位置 |
| 是否可以传递 | 只能通过 Span 传递(不能返回原始 stackalloc) | 整个 struct 可传、可返、可做泛型参数 |
| 典型用户 | 应用开发者写临时缓冲区 | 运行时/库作者暴露高性能 API |
[InlineArray]——这是运行时团队的底层工具。但你写的 SearchValues.Create("abc"u8) 内部就在用它存储字节查找表。你的代码间接受益——更快的字符串搜索、更少的堆分配。
void M(ref readonly int x) 时,调用方怎么写?in 是 C# 7.2 特性(课前基线),本课新增的"深入:防御性拷贝详解"章节也覆盖了核心概念。