可空类型也是值类型,它是包含null值的值类型。例如:int? num=null; int?就是可空的int类型。“?”修饰符只是C#提供的一个语法糖,所谓语法糖,就是C#提供的一种方便的表示形式。C#中肯定没有int?这个类型,对于编译器而言,int?会被编译成Nullable<int>类型,即可空类型。C#2.0中提供的可空类型是Nullable<T>和Nullable(这个T就是泛型参数,由于可空类型的定义是public struct Nullable<T> where T:struct,T只能为值类型)。
Nullable<int> value = null; //上面的代码也可以这样定义 int? num = 500; //HasValue属性指示可空对象是否有值 Console.WriteLine(value.HasValue); //false Console.WriteLine(num.HasValue); //true //GetValueOrDefault() 代表如果可空对象有值,就用它的值返回;如果可空对象不包含值,则返回默认值0 Console.WriteLine(value.GetValueOrDefault()); //输出0 Console.WriteLine(num.GetValueOrDefault()); //输出500 //GetValueOrDefault(T) 方法代表如果HasValue属性为true,则返回value属性的值,否则返回为参数的值 Console.WriteLine(value.GetValueOrDefault(200)); //输出了200 //GetHashCode() 代表如果HasValue属性为true,则value属性返回对象的哈希代码,如果HasValue属性为false,则返回0 Console.WriteLine(num.GetHashCode()); //输出了500 Console.ReadKey();
前面我们讲到,可空类型也是值类型,这个结论是有理有据的,其依据就是可空类型的IL代码,如下图:
空合并操作符
空合并操作符即??操作符,它会对左右两个操作数进行判断:如果左边的数不为null,就返回左边的数;如果左边的数为null,就返回右边的数。这个操作符可以用于可空类型和引用类型,但不能用于值类型。(因为??运算符会将其左边的数与null进行比较,但除了可空类型外,其他的值类型都是不能与null类型进行比较的,所以??运算符不能应用与值类型)。
Nullable<int> n1 = null; int? n2 = 500; Console.WriteLine(n1 ?? n2); //输出了500 // ??与三元运算符功能差不多 int n = n1.HasValue ? n1.Value : 500; //n为500 //同时??运算符也可以用于引用类型 string s = "你不是真正的快乐"; string str = null; Console.WriteLine(s??str); //上面代码等价于: Console.WriteLine(s != null ? s : str); //也等价于:(伪码) //if (s != null) // return s; //else // return str; Console.ReadKey();
从以上代码可以看出,使用??运算符可以很方便地设置默认值,避免了通过if和else语句来判断,从而简化了代码行数,提高了代码的可读性。
可空类型的装箱和拆箱操作
既然值类型存在装箱和拆箱的过程,而可空类型属于值类型,那么它自然也就存在装箱和拆箱的操作了。下面我们一起来看看可空类型的装箱和拆箱的过程。
当一个可空类型赋值给引用类型变量时,CLR会对可空类型(Nullable<T>)对象进行装箱处理。CLR首先会检测可空类型是否为null,如果为null,CLR将不会进行实际的装箱操作(因为null可以赋值给一个引用类型变量);如果不为null,CLR则从可空类型对象中获取值,并对该值进行装箱(即值类型的装箱过程)。
当把一个已装箱的值类型赋值给可空类型变量时,CLR会对已装箱的值类型进行拆箱处理。如果已装箱值类型的引用为null,则CLR会把可空类型也设为null。
Nullable<int> n1 = null; int? n2 = 500; //获得可空对象的类型,此时返回System.Int32,而不是System.Nullable<System.Int32>,这点注意 Console.WriteLine("获取不为null的可空对象的类型:" + n2.GetType()); //输出System.Int32 //对一个为null的类型调用方法时将出现异常,所以一般对引用类型调用方法前,最好先检测下它是否为null //Console.WriteLine("获取为null的可空对象的类型:" + n1.GetType()); //这句将报异常 //将不为null的可空类型对象赋值给引用类型,此时发生装箱操作,可通过IL证明 object obj = n2; Console.WriteLine("获得装箱后引用类型的类型:" + obj.GetType()); //输出System.Int32 //拆箱成非可空变量 int num = (int)obj; Console.WriteLine(num); //500 //拆箱成可空变量 n1 = (int?)obj; Console.WriteLine(n1); //500 int? n3 = null; obj = n3; //对一个没有值的可空类型对象进行装箱操作 Console.WriteLine(obj == null); //true n3 = (int?)obj; //拆箱成可空变量 Console.WriteLine(n3 == null); //true //拆箱成非可空变量,此时会抛异常,因为没有值的可空类型装箱后obj等于null,引用了一个空地址,相当于拆箱后把null赋值给一个int类型的变量,所以会抛异常 //int n4 = (int)obj; Console.ReadKey();
对于可空类型,我们需要注意的地方有以下几点:
1、通过GetType方法来获取赋值的可空类型时,返回的将是赋值的类型。
2、对已赋值的可空类型装箱后,如果使用GetTpye函数去获取装箱后的引用类型,输出的将仍然是已赋值的的类型。
3、如果把一个没有值的可空类型装箱之后再拆箱,不能拆箱为非可空类型的值类型,否则会抛出NullReferenceException异常。
最后还有一点,需要特别注意,对于没有值的可空类型调用函数时会抛出引用异常,那么为什么没有值的可空类型仍然可以访问HasValue属性呢?
原因在于,可空类型是包含null值的可空类型,对于向可空类型赋值这项操作来说,null是一个有效的值类型。而向引用类型赋值null值则表示为空引用,表示不会指向托管堆中的任何对象,所以可以访问HasValue属性。没有值的可空类型在调用GetType函数前,编译器会对可空类型进行装箱操作,使其变为null,即空引用。所以之后再调用GetType函数时,就会抛出空引用异常了。
文章评论