警惕值类型的陷阱
在使用值类型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)。否则后果自负……