警惕值类型的陷阱

使用值类型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)。否则后果自负……

posted @ 2009-12-09 16:37  麒麟.NET  阅读(1881)  评论(14编辑  收藏  举报