关于将readonly struct作为in参数传递的微软编码建议的疑问
这确实是C#里in参数和结构体不可变性容易绕晕的点,我来一步步拆解清楚:
为什么readonly struct能帮in参数避免防御性副本?
首先得明确:in参数是只读引用传递,但这个“只读”是编译器帮你做的语法检查——它禁止你直接给in参数赋值,但管不住结构体内部的实例方法会不会偷偷修改字段。
比如你定义了一个非readonly的结构体,里面有个普通实例方法,这个方法本身是可以修改结构体的字段的(因为值类型的实例方法默认有权限修改自身字段)。这时候编译器犯难了:如果我直接用in引用的原结构体去调用这个方法,万一方法修改了字段,就违反了in参数的只读约定。为了保证安全,编译器只能生成一个防御性副本:把原结构体复制一份,在副本上调用方法,这样原结构体不会被修改,但额外的复制操作就产生了。
而readonly struct就不一样了:编译器会强制这个结构体的所有实例成员都是readonly的(包括自动属性的setter会被禁用,实例方法不能修改字段)。这意味着,任何针对readonly struct的成员调用,都不可能修改它的内部状态。编译器可以100%放心,直接用in引用的原结构体就行,完全不需要生成防御性副本,自然就没了额外的性能开销。
举个直观的代码例子:
// 非readonly可变结构体 struct MutablePoint { public int X; public int Y; // 这个方法会修改结构体字段 public void Move(int dx, int dy) { X += dx; Y += dy; } } void ProcessPoint(in MutablePoint p) { p.Move(1, 1); // 编译器会偷偷生成p的副本,在副本上调用Move,原p完全没变化 } // readonly不可变结构体 readonly struct ImmutablePoint { public int X { get; } public int Y { get; } public ImmutablePoint(int x, int y) => (X, Y) = (x, y); // 不修改原结构体,返回新实例 public ImmutablePoint Move(int dx, int dy) => new ImmutablePoint(X + dx, Y + dy); } void ProcessImmutablePoint(in ImmutablePoint p) { var newPoint = p.Move(1, 1); // 编译器直接用原p的引用,无副本生成 }
非readonly结构体作为in参数,为啥会有性能和行为问题?
性能负面影响
刚才说的防御性副本是核心问题:如果结构体体积较大(比如包含多个数组、大字段),或者你频繁调用它的非readonly成员,每次调用都会产生一次复制,这个开销会比直接按值传递结构体还大——毕竟in参数本来是为了减少复制才设计的,结果反而弄巧成拙。
难以排查的行为问题
更坑的是逻辑不符预期:你以为in参数是引用,操作的是原对象,但实际上操作的是编译器生成的副本,原对象根本没变化。比如这个例子:
struct MutableCounter { public int Count; public void Increment() => Count++; } void UpdateCounter(in MutableCounter counter) { counter.Increment(); // 这里操作的是副本,原counter的Count完全没被修改! } // 调用代码 var c = new MutableCounter(); UpdateCounter(in c); Console.WriteLine(c.Count); // 输出0,而不是你预期的1
这种问题非常隐蔽,代码看起来完全合理,但运行结果就是不对,排查起来特别费劲——因为你很难想到编译器偷偷生成了副本。
总结一下
in参数的“只读”是语法层面的限制,但对于可变结构体,编译器无法保证其成员方法不破坏只读约定,只能通过防御性副本妥协;而readonly struct从类型层面强制了不可变性,让编译器可以安全地跳过副本,既保证性能,又避免了诡异的行为。所以微软才会建议:只有当结构体是readonly,或者你只调用它的readonly成员时,才用in参数传递结构体。
内容的提问来源于stack exchange,提问作者Firld

