为什么说 in 是"只读引用,可能复制"?编译器何时复制、何时不复制?
in 参数修饰符承诺两件事:
| 承诺 | 含义 |
|---|---|
| ✅ 方法不能修改参数值 | 在方法体内对 in 参数赋值是编译错误 |
| ✅ 调用方可传任意表达式 | 字面量、方法返回值、属性、表达式都行——不需要是变量 |
| ❌ 不保证零拷贝 | 编译器可能为了安全而创建防御性拷贝 |
| ❌ 不保证指向原始内存地址 | 拷贝后指向的是临时变量,不是原始位置 |
in = "传引用所以快"。真相是:in 只承诺"方法不会改",不承诺"不复制"。编译器在某些情况下必须复制以确保安全。
问题出在可变 structMutable Struct上。
C# 的 struct 是值类型,但其实例方法可以修改自身(对字段赋值、调用 setter 等)。编译器在看到以下代码时面临两难:
struct Point
{
public int X, Y;
public void Offset(int dx) => X += dx; // 会修改自身!
}
void Process(in Point p)
{
p.Offset(1); // 如果允许直接在原始内存上调用 Offset……
}
如果编译器在原始 p 上调用 Offset(1),就会修改调用方传入的值——这违反了 in 的"只读"承诺。但编译器无法静态分析Offset 的方法体(它可能在另一个程序集中),所以它不能信任 Offset 不会改。
readonly struct 吗?readonly 的成员(C# 8.0+)→ ❌ 不复制readonly)→ ✅ 复制readonly)→ ✅ 复制void Show(in int x) => Console.WriteLine(x);
Show(42); // ✅ 42 复制到临时变量——字面量没有地址
Show(DateTime.Now.Second); // ✅ 表达式结果复制到临时变量
short s = 1;
Show(in s); // ✅ 复制——short→int 需要隐式转换,新临时变量
struct Counter { public int Value; public void Inc() => Value++; }
void Use(in Counter c)
{
c.Inc(); // ⚡ 防御性拷贝!编译器创建临时 Counter,在其上调用 Inc()
// 调用方的原始 c.Value 不变——Inc 修改的是副本
Console.WriteLine(c.Value); // ⚡ 又一次防御性拷贝!getter 没标 readonly
}
var c = new Counter { Value = 0 };
Use(in c);
Console.WriteLine(c.Value); // 输出 0——原始值没变(Inc 改的是副本)
struct Vector3 { public float X, Y, Z; }
float Dot(in Vector3 a, in Vector3 b)
=> a.X * b.X + a.Y * b.Y + a.Z * b.Z; // ❌ 零拷贝——纯字段访问,编译器不担心
readonly struct ReadOnlyPoint
{
public int X { get; } // auto-property getter 隐式 readonly
public int Y { get; }
public double Distance => Math.Sqrt(X*X + Y*Y);
}
void Process(in ReadOnlyPoint p)
{
var d = p.Distance; // ❌ 零拷贝——编译器知道整个 struct 不可变
Console.WriteLine(p.X); // ❌ 零拷贝
}
struct HybridPoint // 可变 struct
{
private int _x, _y;
public int X { get => _x; set => _x = value; }
public int Y { get => _y; set => _y = value; }
public readonly double Distance // readonly 承诺"我不会改"
=> Math.Sqrt((double)_x * _x + (double)_y * _y);
}
void Process(in HybridPoint p)
{
var d = p.Distance; // ❌ 零拷贝——Distance 标了 readonly
Console.WriteLine(p.X); // ⚡ 复制!getter 没标 readonly,编译器不信任
}
以下 C# 代码编译为 IL:
// C#: void Bad(in Point p) { Console.WriteLine(p.X); }
// IL:
ldarg.1 // 加载 in 参数的地址
ldobj Point // 从该地址复制整个值到求值栈 ← 这就是防御性拷贝
stloc.0 // 存储到临时局部变量
ldloca.s 0 // 加载临时变量的地址
call instance int32 Point::get_X()
call void Console::WriteLine(int32)
// C#: void Good(in ReadOnlyPoint p) { Console.WriteLine(p.X); }
// IL:
ldarg.1 // 加载 in 参数的地址
call instance int32 ReadOnlyPoint::get_X()
call void Console::WriteLine(int32)
关键区别:有防御性拷贝时多出 ldobj + stloc 对,这组指令完成了"读整个 struct → 写到临时变量"的复制过程。
in 可能比直接按值传递更慢。原因:按值传递只复制一次(调用时),但 in 传可变 struct 可能每次成员访问都复制——在循环和热路径中这是灾难。
struct BigMutable { /* 很多字段 */ public int Sum => /* 复杂计算 */; }
// ❌ 坏:每次循环都防御性拷贝一次(访问 Sum getter)
int TotalLoop(in BigMutable[] items)
{
int total = 0;
foreach (ref readonly var item in items)
total += item.Sum; // ⚡ 每次迭代都复制!
return total;
}
// ✅ 好:按值传递——只在调用时复制一次
int TotalLoop(BigMutable[] items) { /*……*/ }
| 参数类型 | 你做的操作 | 防御性拷贝? | 说明 |
|---|---|---|---|
| 可变 struct(普通 struct) | 访问字段 | ❌ 不复制 | 直接字段读取,编译器确定安全 |
调用 readonly 成员 | ❌ 不复制 | C# 8.0+,成员标记了 readonly | |
| 访问属性 / getter(未标 readonly) | ✅ 复制 | 编译器不能信任 getter 不改状态 | |
| 调用实例方法(未标 readonly) | ✅ 复制 | 经典防御性拷贝触发场景 | |
readonly struct | 任意成员访问 | ❌ 不复制 | 编译器确定整个 struct 不可变 |
| 调用任意方法 | ❌ 不复制 | 同上 | |
| 非变量实参 | 字面量(in 42) | ✅ 复制 | 字面量没有地址 |
表达式(in (a+b)) | ✅ 复制 | 表达式结果没有地址 | |
类型不匹配(short 传 in int) | ✅ 复制 | 隐式转换产生新临时变量 | |
| in 参数传递给另一位 in | 引用透传 | ❌ 不复制 | 引用直接转发 |
Nullable<T> | 任何操作 | ✅ 复制 | Nullable<T> 不是 readonly struct |
in | ref readonly C# 12 | |
|---|---|---|
| 承诺 | 只读(可能通过复制来保证) | 只读(通过禁止修改 + 保证不复制来保证) |
| 指向 | 可能是原始地址,也可能是临时变量 | 一定是原始地址 |
| 实参要求 | 任意表达式 | 必须是变量(有地址) |
| 调用方语法 | in 可省略 | 必须写 ref |
| 适用场景 | 大 struct 性能传参、API 用户友好 | 需要确切地址的场景(互操作、安全代码) |
| 防御性拷贝 | 视情况 | 不会——因为必须是变量 + 编译器有完整信息 |
in 和 readonly struct 配对使用。只有这样才能同时得到"只读保证"和"零拷贝性能"。readonly,至少把无副作用的成员标上 readonly(C# 8.0+)。in——性能可能反而比按值传递更差(每次成员访问都防御性拷贝 vs 调用时拷贝一次)。Nullable<T> 用 in——它不是 readonly struct。IntPtr.Size(4 字节在 32 位 / 8 字节在 64 位)的类型,in 的性能收益微乎其微。ref readonly,不要用 in。in 的"可能复制" = 当编译器不能证明 struct 不可变时,它会在每次成员访问(方法调用、属性 getter)时先复制再操作——语义安全但性能有坑。消除防御性拷贝的唯一办法是用 readonly struct 或 readonly 成员。