警惕值类型的陷阱(补充)
在警惕值类型的陷阱中,乌卡卡同学指出第一个示例之所以出现问题,是因为委托的实现方式不对。
我能想到的“委托的实现方式”除了这种正常的赋值,剩下的就是用Lambda表达式或匿名方法了了,于是我把代码改为如下的形式:
struct MyStruct { public int value; public void SetValue(int value) { this.value = value; } } class Program { static void Main() { var ms = new MyStruct(); Action<int> action = (i) => ms.SetValue(i); action(1); Console.WriteLine(ms.value); Console.ReadLine(); } }
运行,奇迹发生了,结果是1!
为什么改成Lambda表达式之后结果完全不一样了呢?我本能的反应是没有发生装箱操作,但一时还搞不明白到底是怎么回事,于是迫不及待地打开IL查看:
.method private hidebysig static void Main() cil managed { .entrypoint // Code size 65 (0x41) .maxstack 3 .locals init ([0] class [mscorlib]System.Action`1action, [1] class MyTest.Program/'<>c__DisplayClass1' 'CS$<>8__locals2') IL_0000: newobj instance void MyTest.Program/'<>c__DisplayClass1'::.ctor() IL_0005: stloc.1 IL_0006: nop IL_0007: ldloc.1 IL_0008: ldflda valuetype MyTest.MyStruct MyTest.Program/'<>c__DisplayClass1'::ms IL_000d: initobj MyTest.MyStruct IL_0013: ldloc.1 IL_0014: ldftn instance void MyTest.Program/'<>c__DisplayClass1'::'b__0'(int32) IL_001a: newobj instance void class [mscorlib]System.Action`1 ::.ctor(object, native int) IL_001f: stloc.0 IL_0020: ldloc.0 IL_0021: ldc.i4.1 IL_0022: callvirt instance void class [mscorlib]System.Action`1 ::Invoke(!0) IL_0027: nop IL_0028: ldloc.1 IL_0029: ldflda valuetype MyTest.MyStruct MyTest.Program/'<>c__DisplayClass1'::ms IL_002e: ldfld int32 MyTest.MyStruct::'value' IL_0033: call void [mscorlib]System.Console::WriteLine(int32) IL_0038: nop IL_0039: call string [mscorlib]System.Console::ReadLine() IL_003e: pop IL_003f: nop IL_0040: ret } // end of method Program::Main
MyTest.MyStruct MyTest.Program/'<>c__DisplayClass1'是编译器为Lambda表达式自动生成的类。因为Lambda表达式也好,匿名方法也好,都需要存在于类之中。上面的代码与下面是一样的:
Action<int> action = delegate(int i) { ms.SetValue(i); };
看到这里,我想您应该已经明白了。这里用于委托_target字段的是匿名方法所在的类<>c__DisplayClass1,而不是装箱之后的MyStruct,因此不存在装箱操作。我们只是在匿名方法内部调用了SetValue方法,并把该匿名方法作为委托的方法,而不是SetValue方法。也就是说委托的Invoke调用的是匿名方法,而不是SetValue方法。
我想这就应该是乌卡卡同学所指出的,换一种委托形式以避免装箱的方法吧。