.NET,你忘记了么?(七)—— 从a?b:c谈起

1.  摘要

在这篇文章中,我会通过IL去分析一个简单的语句。

如果觉得实在简单,可以略过。

2.  引子

事情是这样的,同事写了一段类似这样的代码:

class Program
{
      static void Main(string[] args)
      {
            object o = new object();
            int i;
            Int32.TryParse(Console.ReadLine(), out i);
            o = i > 3 ? null : 3.5;
      }
}

当然不是在控制台程序中,我在这里只是写出个模拟。

然后系统报出了一个这样的错误。

image

3. 错误分析

同事很诧异地问我,这是为什么啊?

他给出的理由是object是一切类的父类,那么我把3.5或者null赋给他都没有问题啊,那这个问题是怎么回事呢?

我意识到自己的语言表达能力远不如代码有说服力,于是,写段代码,然后请出IL。

4. 请出IL

让我们先写段正确的代码,保证他的编译通过。

class Program
{
      static void Main(string[] args)
      {
            object o = new object();
            int i = 1;
            int j = 2;
            o = i+j > 3 ? 3 : 3.5;
      }
}

然后去查看IL代码:

.method private hidebysig static void Main(string[] args) cil managed
{
      .entrypoint
      .maxstack 2
      .locals init (
      [0] object o,
      [1] int32 i,
      [2] int32 j)
      L_0000: nop
      L_0001: newobj instance void [mscorlib]System.Object::.ctor()
      L_0006: stloc.0
      L_0007: ldc.i4.1
      L_0008: stloc.1
      L_0009: ldc.i4.2
      L_000a: stloc.2
      L_000b: ldloc.1
      L_000c: ldloc.2
      L_000d: add
      L_000e: ldc.i4.3
      L_000f: bgt.s L_001c
      L_0011: ldc.r8 3.5
      L_001a: br.s L_0025
      L_001c: ldc.r8 3
      L_0025: box float64
      L_002a: stloc.0
      L_002b: ret
}

在这里,我们只关注从L_00d开始的代码,首先我们将两个数i和j相加,然后去与3比较大小,如果大于3,那么便跳转到L_001c,将3作为Float类型压栈,否则顺序向下执行,将3.5作为float类型压栈。最后将栈顶元素装箱。

看过了这个解释,我们再回去看原有的那段代码,原因再清楚不过了,?:这个三元运算符在编译成IL代码时,把:两端的值压栈,然后把这两个值存储在一个临时变量里,而这个变量要取两者之前类型转换后级数最高的类型。举个例子:int 和 float 就需要转换成float,float 和 double 就需要转换成double 等等。而在同事的程序中, double 类型和 null 类型无法相互转换,所以就报了这样的一个错误。

5. 改造代码

继续仔细思考,究竟什么样的两个类型可以写在:的两端。上面的错误再清楚不过。两个可以隐式转换的类型可以。

那下面继续解决这个问题。上面的代码我们要怎么写:

static void Main(string[] args)
{
      object o = new object();
      int i;
      Int32.TryParse(Console.ReadLine(), out i);
      if (i > 3)
      {
            o = null;
      }
      else
      {
            o = 3.5;
      }
}

怎么看都没有上面的代码漂亮。这个可以说除了能运行外真的没什么优点了。

还记得那个泛型类吧:Nullable<T>。

那就让我们用这个泛型类来改造吧:

static void Main(string[] args)
{
      object o = new object();
      Nullable<double> n = 3.5;
      int i;
      Int32.TryParse(Console.ReadLine(), out i);
      o = i > 3 ? null : n;
}

如果觉得Nullable<T>还不够美观。

static void Main(string[] args)
{
      object o = new object();
      double? n = 3.5;
      int i;
      Int32.TryParse(Console.ReadLine(), out i);
      o = i > 3 ? null : n;
}

这样改造是不是优秀了一些呢?

这个时候如果有人提出,那么我为什么不o=i>3?null:(object)3.5呢?那我们想一下如果有一天我们不再用object o;

我们是不是可以把代码写成这样:

static void Main(string[] args)
{
    //object o = new object();
    double? o;
    double? n = 3.5;
    int i;
    Int32.TryParse(Console.ReadLine(), out i);
    o = i > 3 ? null : n;
}

ShadowK 给出了这样的做法,是个好办法:

o = i > 3 ? (double?)null : n ;  

这个时候再看IL,是不是已经没有了可恶的box呢?

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] valuetype [mscorlib]System.Nullable`1<float64> o,
        [1] valuetype [mscorlib]System.Nullable`1<float64> n,
        [2] int32 i,
        [3] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0000)
    L_0000: nop 
    L_0001: ldloca.s n
    L_0003: ldc.r8 3.5
    L_000c: call instance void [mscorlib]System.Nullable`1<float64>::.ctor(!0)
    L_0011: nop 
    L_0012: call string [mscorlib]System.Console::ReadLine()
    L_0017: ldloca.s i
    L_0019: call bool [mscorlib]System.Int32::TryParse(string, int32&)
    L_001e: pop 
    L_001f: ldloc.2 
    L_0020: ldc.i4.3 
    L_0021: bgt.s L_0026
    L_0023: ldloc.1 
    L_0024: br.s L_002f
    L_0026: ldloca.s CS$0$0000
    L_0028: initobj [mscorlib]System.Nullable`1<float64>
    L_002e: ldloc.3 
    L_002f: stloc.0 
    L_0030: ret 
}

6. 总结

写上面的文章我并不是单纯地想阐明这个具体的语法情况。而是希望大家掌握一种思路。

语法怎么回事?为什么不是像我想的那样?

请出IL。

posted @ 2009-04-24 01:19  飞林沙  阅读(3228)  评论(29编辑  收藏  举报