.NET为何为JIT方法中的readonly struct生成防御性副本?
首先要明确:readonly struct的readonly修饰符和参数传递的副本机制是两个独立的概念,不能混为一谈。
核心原因:
值类型的默认传递规则
.NET中值类型(包括readonly struct)作为方法参数时,默认遵循按值传递的规则——不管结构体有没有readonly修饰,都会创建一个完整的实例副本,传递给目标方法的栈帧。这是CLR的基础调用约定,目的是保持值类型的“值语义”:每个方法拿到的都是独立的实例,确保方法内的操作不会意外影响到外部的原实例。readonly struct的语义边界
readonly修饰符的作用是约束结构体自身的成员无法被修改,同时告诉编译器可以做一些内存优化(比如紧凑布局、避免生成写操作指令),但它并没有改变值类型参数传递的默认行为。也就是说,readonly只负责“限制结构体实例的可修改性”,不负责“改变参数传递的方式”。
结合你的示例分析
你的代码里,M方法调用M1(Foo f)时,因为是默认按值传递,CLR会把M栈帧里的Foo实例完整复制到M1的栈帧中——对应汇编里的vmovdqu等复制指令。而如果把M1的参数改成in Foo f,就会切换为只读引用传递,CLR直接传递原实例的地址,不需要复制,自然也就没有防御性副本了。
为什么不直接传递栈指针?
默认按值传递是为了坚守值类型的设计初衷:值类型的实例是“值”,不是“引用”,每个方法都应该拿到属于自己的独立副本。如果默认就传递栈指针(引用),会打破值语义的一致性,即使readonly struct本身不可修改,也会让值类型的行为变得和引用类型混淆,违背.NET类型系统的设计原则。
内容的提问来源于stack exchange,提问作者Irdis

