一,可空值类型
顾名思义,这种类型支持null值。在FCL中被定义为System.Nullable<T>类型。这是一个结构体类型(struct),所以,不要认为System.Nullable<T>是一个引用类型,它仍然是一个值类型。
要使用可空值类型,在代码中可以这样写:
Nullablex = 10; Nullable 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=10y: 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转换成NullableInt32? b = null; //从‘null’隐式转换为Nullable Int32 c = (Int32)a; //从Nullable 显示转换为Int32 Double? d = 5; //从Int32转型为Nullable (d是double值5.0) Double? e = b; //从Nullable 转型为Nullable (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";
有人认为??操作符只不过是三元操作符?:的“语法糖”而已。事实上,??操作符提供了重大的语法上的改进,第一个改进就是更好的支持表达式。
Funcf = () => SomeMethod() ?? "unknown";
相比下一行代码,上述代码更容易理解,下面的代码要求进行变量赋值,而且一个语句还搞不定:
Funcf = () => { 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? a = (Int32?)o; Int32 b = (Int32)o; //创建初始化为null的一个引用 o = null; //把它拆箱为一个Nullable 和一个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.NullableConsole.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。