转自:http://www.cnblogs.com/kirinboy/archive/2009/12/09/be-careful-of-the-trap-of-value-type.html
在使用值类型LazyString分析字符串的评论中,有人贴出了这样两段有意思的代码。我们来逐一分析。
代码1:
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 = ms.SetValue; action(1); Console.WriteLine(ms.value); } }
输出结果:
0
而如果去掉委托,直接SetValue,结果当然就是1了。为什么加了一个委托结果就完全不同了呢?
我们知道,委托内部有一个object类型的_target字段,用来指明委托所调用的方法所在的实例类型。在这个例子中,_target就是ms。而ms是值类型的,因此这里会存在一个装箱操作。委托所调用的是装箱之后的引用类型的SetValue方法,这只会影响装箱之后的引用类型,并不会影响装箱之前的值类型。因此ms.value仍然为0。
IL如下:
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 55 (0x37)
.maxstack 3
.locals init ([0] valuetype MyTest.MyStruct ms,
[1] class [mscorlib]System.Action`1<int32> action)
IL_0000: nop
IL_0001: ldloca.s ms
IL_0003: initobj MyTest.MyStruct
IL_0009: ldloc.0
IL_000a: box MyTest.MyStruct
IL_000f: ldftn instance void MyTest.MyStruct::SetValue(int32)
IL_0015: newobj instance void class [mscorlib]System.Action`1<int32>::.ctor(object,
native int)
IL_001a: stloc.1
IL_001b: ldloc.1
IL_001c: ldc.i4.1
IL_001d: callvirt instance void class [mscorlib]System.Action`1<int32>::Invoke(!0)
IL_0022: nop
IL_0023: ldloca.s ms
IL_0025: ldfld int32 MyTest.MyStruct::'value'
IL_002a: call void [mscorlib]System.Console::WriteLine(int32)
IL_002f: nop
IL_0030: call string [mscorlib]System.Console::ReadLine()
IL_0035: pop
IL_0036: ret
} // end of method Program::Main
代码2:
struct MyStruct { public int value; public int Increment() { return ++value; } } class Program { static readonly MyStruct ms; static void Main() { Console.WriteLine(ms.Increment()); Console.WriteLine(ms.Increment()); } }
输出结果:
1
1
为什么两次都是1呢?是第一次Increment起作用了,而第二次没有起作用,还是两次都起作用了但是没有对ms.value进行修改呢?我们还是请出IL来说话:
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 40 (0x28)
.maxstack 1
.locals init ([0] valuetype MyTest.MyStruct CS$0$0000)
IL_0000: nop
IL_0001: ldsfld valuetype MyTest.MyStruct MyTest.Program::ms
IL_0006: stloc.0
IL_0007: ldloca.s CS$0$0000
IL_0009: call instance int32 MyTest.MyStruct::Increment()
IL_000e: call void [mscorlib]System.Console::WriteLine(int32)
IL_0013: nop
IL_0014: ldsfld valuetype MyTest.MyStruct MyTest.Program::ms
IL_0019: stloc.0
IL_001a: ldloca.s CS$0$0000
IL_001c: call instance int32 MyTest.MyStruct::Increment()
IL_0021: call void [mscorlib]System.Console::WriteLine(int32)
IL_0026: nop
IL_0027: ret
} // end of method Program::Main
可见,编译器创建了一个类型为为MyStruct临时变量,在每次调用ms.Increment之前,都会加载这个临时变量。这样每次执行方法所使用的ms都是该临时变量,Increment方法所做的value值的修改,并不能反映到该临时变量上去,因为这个临时变量是不变的(只读)。
由以上两个例子可以看出,值类型经常产生我们意想不到的结果,因此在自定义值类型时,一定要设计为不可变的(Immutable)。否则后果自负……
(补充):
在警惕值类型的陷阱中,乌卡卡同学指出第一个示例之所以出现问题,是因为委托的实现方式不对。
我能想到的“委托的实现方式”除了这种正常的赋值,剩下的就是用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`1 action, [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方法。
我想这就应该是乌卡卡同学所指出的,换一种委托形式以避免装箱的方法吧。