雁过请留痕...
代码改变世界

《CLR via C#》笔记——可空值类型

2012-07-20 18:15  xiashengwang  阅读(457)  评论(0编辑  收藏  举报

一,可空值类型

  顾名思义,这种类型支持null值。在FCL中被定义为System.Nullable<T>类型。这是一个结构体类型(struct),所以,不要认为System.Nullable<T>是一个引用类型,它仍然是一个值类型

要使用可空值类型,在代码中可以这样写:

            Nullable<Int32> x = 10;
            Nullable<Int32> y = null;
            Console.WriteLine("x: HasValue={0} Value={1}", x.HasValue, x.Value);
            Console.WriteLine("y: HasValue={0} Value={1}", y.HasValue, y.GetValueOrDefault());

得到的输出如下:

x: HasValue=True Value=10
y: HasValue=False Value=0

二,C#对可空值类型的支持

    C#开发团队希望将可空值类型集成到C#语言中,为此,C#提供了一个更简单清晰的语法来处理可空值类型。可以使用类型加?号的方式来表示一个可空值类型。如下:

            Int32? x = 10;
            Int32? y = null;

在C#中,Int32?等价于System.Nullable<Int32>。但是C#在此基础上更进一步,允许开发人员在可空类型上执行转换和转型。转换:如Int32? ⇒ Int32。转型:如Int32? ⇒ Double?。

            Int32? a = 5; //从非可空Int32转换成Nullable<Int32>
            Int32? b = null; //从‘null’隐式转换为Nullable<Int32>
            Int32 c = (Int32)a; //从Nullable<Int32>显示转换为Int32

            Double? d = 5; //从Int32转型为Nullable<Double>(d是double值5.0)
            Double? e = b; //从Nullable<Int32>转型为Nullable<Double>(e是null)

C#还允许向可空实例应用操作符。下面是一些例子:

            Int32? a = 5;
            Int32? b = null;

            //一元操作符(+ ++ - -- ! ~)
            a++;    //a = 6
            b = -b; //b = null

            //二元操作符(+ - * / % & | ^ << >>)
            a = a + 3;  //a = 9
            b = b * 3; //b = null
            Int32 c = (Int32)a;

            //相等操作符(== !=)
            if (a == null) { /* no */ } else { /* yes */}
            if (b == null) { /* yes */ } else { /* no */}
            if (a != null) { /* yes */}

            //比较操作符(< > <= >=)
            if (a < b) { /* no */ } else { /* yes */}

●一元操作符(+,++,-,--,!,~) 操作数是null,结果就是null。

●二元操作符(+,-,*,/,%,&,|,^,>>,<<) 两个操作数中任何一个是null,结果就是null。但有一个例外,它发生在&和|操作符应用于Boolean?操作数时。在这种情况下,这两个操作符的行为和SQL的三值逻辑是一致的。(为了便于理解,可以将这里的null理解为UnKnown,一个非null操作数遇到一个null(UnKnown)操作数,具体要看两个数能不能确定一个值,如果不能确定就是null(UnKnown)。比如A是true,B是null,那么A&B的结果是什么呢?由于B是不确定的,可以是false或true,那么A&B的结果可能是false,也可能是true,这样一来,A&B的结果就被认为是不确定的,于是就是null。而A|B的结果,不管B是false还是true,由于A是true,可以推断出A|B的结果就是True。)

●相等符操作符(==,!=)两个操作符都是null,两者相等。一个操作数是null,则两者不相等。两个操作数都不是null,就比较值来判断是否相等。

●关系操作符(<,>,<=,>=) 两个操作符任何一个是null,结果就是false。两个操作数都不是null,就比较值。

三,C#的空接合操作符(??

    C#提供了一个所谓的“空接合操作符”(null-coalescing operator),即??操作符,它要获取两个操作数。假如左边的操作数不是null,就返回这个操作数的值。如果左边的操作数是null,就返回右边这个操作数的值。利用空接合操作符可以方便的设置变量的默认值。它的另一个妙处在于,它既能用于引用类型,也能用于可空值类型。下面的代码演示了这种用法:

            Int32? b = null;

            Int32? x = b ?? 123;
            //上面折行等价于
            //x = b.HasValue ? b.Value : 123;

            string fileName = GetFileName() ?? "untitled";
            //上面这行等价于
            //string temp = GetFileName();
            //fileName = temp != null ? temp : "untitled";  

有人认为??操作符只不过是三元操作符?:的“语法糖”而已。事实上,??操作符提供了重大的语法上的改进,第一个改进就是更好的支持表达式。

Func<string> f = () => SomeMethod() ?? "unknown";

相比下一行代码,上述代码更容易理解,下面的代码要求进行变量赋值,而且一个语句还搞不定:

            Func<string> f = () =>
            {
                var temp = SomeMethod();
                return temp != null ? temp : "unknown";
            };

第二个改进是??在复合的情形中更好用。例如:

string s = SomeMethod1() ?? SomeMethod2() ?? "unknown";

它比下面这一堆代码更容易阅读和理解。

            string s;
            var sm1 = SomeMethod1();
            if (sm1 != null) s = sm1;
            else
            {
                var sm2 = SomeMethod2();
                if (sm2 != null) s = sm2;
                else
                    s = "unknown";
            }

四,CLR对可空值类型的特殊支持

4.1,可空值类型的装箱

CLR对一个Nullable<T>实例装箱时,他会检查它是否为null。如果是,则不实际装箱任何东西,并返回null值。如果实例不为null,CLR从可空实例中取出值,并对其进行装箱。

            Int32? n = null;
            object o = n; //o为null
            Console.WriteLine("o is null = {0}", o == null);//true
            n = 5;
            o = n; //o引用了一个已装箱的Int32
            Console.WriteLine("o' type {0}", o.GetType());//"System.Int32"

4.2,可空值类型的拆箱

CLR允许将一个已装箱的值类型T拆箱成为一个T或者一个Nullable<T>。如果对已装箱的类型的引用是null,而且要把它拆箱为一个Nullable<T>,那么CLR会将Nullable<T>的值设为null。下面的代码进行了演示:

            object o = 5;
            //它拆箱为一个Nullable<Int32>和一个Int32
            Int32? a = (Int32?)o;
            Int32 b = (Int32)o;

            //创建初始化为null的一个引用
            o = null;
            //把它拆箱为一个Nullable<Int32>和一个Int32
            a = (Int32?)o; // a = null
            b = (Int32)o;  //NullReferenceException

4.3,通过可空值类型调用GetType

在一个Nullable<T>对象上调用GetType时,CLR实际会“撒谎”说类型是T,而不是Nullable<T>。下面的代码演示了这一行为:

            Int32 x = 5;
            //下面这行会显示“System.Int32”,而非“System.Nullable<Int32>
            Console.WriteLine(x.GetType());

4.4,通过可空值类型调用接口

下面的代码,将一个Nullable<Int32>类型的变量转型为一个IComparable<Int32>接口类型,但Nullable<T>不像Int32那样实现了IComparable<Int32>接口。C#编译器允许这样的代码通过编译,而且CLR的验证器认为这是可以验证的,从而允许我们使用一种更简洁的语法:

            Int32? a = 5;
            Int32 result = ((IComparable)a).CompareTo(5);//能通过编译和运行
            Console.WriteLine(result);//0

假如CLR没有提供这个支持,就要写非常繁琐的代码。首先必须转型成Int32,再转型成接口以发出调用:

Int32 result = ((IComparable)(Int32)a).CompareTo(5);//这太繁琐了

4.5,语法糖陷阱

            int? b = null;
            int? c = new int?();

上面两句代码是等价的,int? b = null;被编译后会变成int? b = new int?();

int? 可空类型是值类型,这里的null值并不等同于引用类型的null,这里要特别的注意。

可空类型null值的比较只是编译器帮我们做了额外的工作,不要被它欺骗了。如下面的判断:

            if (a == null)
            { }
            if (a.HasValue)
            { }

不要天真的以为a==null这句和我们平常写的一个对象为null的判断是等价的,那你就错了。

真实的情况是,上面的代码被编译后被翻译成了:

    if (!a.HasValue)
    {
    }
    if (a.HasValue)
    {
    }

a==null被编译成了a.HasValue。这也就解释了为什么引用类型的变量为null时,不能够调用其成员,会报一个NullReferenceException的异常。

而可空类型即使你对它赋值为null,也可以调用它的成员,如a.HasValue,a.Equals等方法。

这全是C#的编译器的语法糖。可空类型是值类型,它永远会有一个结构,而不会为空引用null。