使用当前的编译器,防御性副本确实似乎是为“原始”值类型和其他非只读结构制作的。具体来说,它们的生成方式与readonly 字段的生成方式类似:当访问可能会改变内容的属性或方法时。副本出现在每个调用站点给一个潜在的变异成员,所以如果你调用 n 个这样的成员,你最终会让 n 防御副本。与readonly 字段一样,您可以通过手动将原件复制到本地来避免多次复制。
看看this suite of examples。您可以查看 IL 和 JIT 程序集。
通过 in 参数传递原始类型并且不制作防御性副本是否安全?
这取决于您是否访问in 参数上的方法或属性。如果这样做,您可能会看到防御性副本。如果没有,您可能不会:
// Original:
int In(in int _) {
_.ToString();
_.GetHashCode();
return _ >= 0 ? _ + 42 : _ - 42;
}
// Decompiled:
int In([In] [IsReadOnly] ref int _) {
int num = _;
num.ToString(); // invoke on copy
num = _;
num.GetHashCode(); // invoke on second copy
if (_ < 0)
return _ - 42; // use original in arithmetic
return _ + 42;
}
其他常用的框架结构,例如 DateTime、TimeSpan、Guid,... 是否被 [编译器] 视为只读?
不,仍将在调用站点为这些类型的 in 参数上可能发生变异的成员制作防御性副本。然而,有趣的是,并非所有方法和属性都被认为是“潜在的变异”。我注意到,如果我调用默认方法实现(例如,ToString 或 GetHashCode),则不会发出防御性副本。但是,一旦我覆盖了这些方法,编译器就会创建副本:
struct WithDefault {}
struct WithOverride { public override string ToString() => "RO"; }
// Original:
void In(in WithDefault d, in WithOverride o) {
d.ToString();
o.ToString();
}
// Decompiled:
private void In([In] [IsReadOnly] ref WithDefault d,
[In] [IsReadOnly] ref WithOverride o) {
d.ToString(); // invoke on original
WithOverride withOverride = o;
withOverride.ToString(); // invoke on copy
}
如果这因平台而异,我们如何确定在给定情况下哪些类型是安全的?
嗯,所有类型都是“安全的”——副本确保了这一点。我假设您在问哪些类型会避免防御性副本。正如我们在上面看到的,它比“参数的类型是什么”更复杂?没有单一副本:副本在对 in 参数的某些引用处发出,例如,引用是调用目标的地方。如果不存在此类引用,则无需制作副本。此外,是否复制的决定取决于您调用的是已知安全或“纯”的成员,还是调用可能改变 a 值类型内容的成员。
目前,某些默认方法似乎被视为纯方法,编译器会避免在这些情况下进行复制。如果我不得不猜测,这是预先存在的行为的结果,编译器正在利用一些最初为readonly 字段开发的“只读”引用概念。正如您在下面看到的(或in SharpLab),行为是相似的。注意 IL 在调用WithDefault.ToString 时如何使用ldflda(加载字段按地址)将调用目标压入堆栈,但使用ldfld、stloc、ldloca 序列在调用WithOverride.ToString 时将副本 压入堆栈:
struct WithDefault {}
struct WithOverride { public override string ToString() => "RO"; }
static readonly WithDefault D;
static readonly WithOverride O;
// Original:
static void Test() {
D.ToString();
O.ToString();
}
// IL Disassembly:
.method private hidebysig static void Test () cil managed {
.maxstack 1
.locals init ([0] valuetype Overrides/WithOverride)
// [WithDefault] Invoke on original by address:
IL_0000: ldsflda valuetype Overrides/WithDefault Overrides::D
IL_0005: constrained. Overrides/WithDefault
IL_000b: callvirt instance string [mscorlib]System.Object::ToString()
IL_0010: pop
// [WithOverride] Copy original to local, invoke on copy by address:
IL_0011: ldsfld valuetype Overrides/WithOverride Overrides::O
IL_0016: stloc.0
IL_0017: ldloca.s 0
IL_0019: constrained. Overrides/WithOverride
IL_001f: callvirt instance string [mscorlib]System.Object::ToString()
IL_0024: pop
IL_0025: ret
}
也就是说,既然只读引用可能会变得更加普遍,那么可以在没有防御性副本的情况下调用的方法的“白名单”将来可能会增加。就目前而言,这似乎有些武断。