1 引入可空类型
可空类型的声明方法是在基础类型之上加上一个问号"?"。
int ? i; i=10;
C#中,只有值类型才有可空类型(引用类型可以取null值),其中包括系统预定义的整数类型、字符类型、实数类型、布尔类型,以及各种结构类型和枚举类型。
2 泛型结构NullableType
2.1 概述
.NET是如何实现可空类型呢?一种设想是对每个值类型都定义一个新的可空类型,但这种做法工作量大,且没有扩展性:开发人员新定义的值类型就无法享受这一好处,除非他们每次都成对地定义值类型和可空类型。
.NET类库利用泛型的强大功能,一劳永逸地为所有值类型实现了可空类型。System中定义了一个泛型结构NullableType<T>,上一节采用的实际上是对可空类型的简写方式。int ? 等同于NullableType<int>。NullableType<T>的简写方式就是T?。
这种简写方式既可用于可空类型的声明,也可用于可空类型的构造。泛型结构NullableType<T>可实际定义的构造函数的原型为public Nullable<T>,所以可采用下面的代码来构造可空类型的实例:
int ? i = new int ?(5);//i=5; Nullable<int> j = new Nullable<int>(10);//j=10; Nullable<DateTime> dt = new DateTime?(DateTime.Now);//dt=Now
不过这种方式不适用于值为null的情况,因数NullableType<T>的构造函数的参数类型要求为T。下面的代码是不合法的:
int ? i=new int?(null);//error
可空类型是值类型和空值null的复合,但它在本质上还是一种值类型。可空类型的变量也是直接包含自身的数据,而不是指向目标数据的引用。因此可空类型本身也可以作为一种基础类型来构造新的可空类型。下面的代码都是合法的:
int? i = 10; int?? j = i; int??? k = j; Nullable<double> x = 3.14; Nullable<Nullbale<double>> y=x;
2.2 判断和取值
给定一个可空类型的变量,要知道它是不是空值null,可通过NullableType<T>的公用属性HasValue来确定。该属性为布尔值。
NullableType<T>的另一个公有属性Value的类型为T,它表示可空类型对应的基础类型的值。该属性仅在变量不为空值时有效,因此访问之前要检查属性HasValue的值是否为True。例如下面的代码会引发异常:
int? i = null; Console.WriteLine(i.Value);
正确的写法是:
if(i.HasValue) Console.WriteLine(i.Value);
HasValue和Value都是只读的,因此可空类型不支持通过Value属性来修改自身的值。考虑下面的结构定义:
public struct Number { //字段 private int m_value; //属性 public int Value { get { return m_value; } set { m_value1=value; } } }
尽管结构Number的属性Value是可以修改的,但对于可空类型Number?,却不能使用下面的代码来修改该属性:
Number? n1= new Number(); n1.Value.Value = 5;//error
可空类型只能读取,不能修改其基础类型中定义的公有字段和属性。解决的办法只能是将可空类型与另一个基础类型的变量相关联。上面的代码应改写为:
Number tmp = new Number(); tep.Value = 5; Number? n1 = tmp;
属性HasValue的名称会带来一点点混淆,实际上它并不是指可空类型有没有值,而是指可空类型的值是否在基础类型的范围之内。如果值为null,则属性HasValue返回False;而如果没有值,就根本不能调用该属性。所以可空类型的变量同样应当遵循先赋值后使用的原则,如下面的代码就是错误的,因为变量i没有被始化化:
int? i; if(i.HasValue) //error;将引发异常 ...
而对于可空类型的字段成员,如果在对象中一直没有被初始化,那么该字段的默认值为null,这一点和普通的值类型是不一样的。下面的程序说明了这一点:
class NullableDefaultSample { static void Main() { Class1 c1 = new Class1(); c1.Output(); } public class Class1 { //字段 private int m_commonValue; private int? m_nullableValue; //方法 public void Output() { Console.WriteLine(m_commonValue); Console.WriteLine(m_nullableValue); Console.WriteLine(m_nullableValue == null); } } }
编译器在编译上面的程序时,会给出变量没有被赋值的警告信息。程序输出结果为:int类型字段的默认值为0,而int?类型字段的默认值为null:
0 True 请按任意键继续. . .
泛型结构NullableType<T>还重载了相等和不等操作符,其原型分别为:
public static bool operator == (NullableType<T>,NullableType<T>) public static bool operator != (NullableType<T>,NullableType<T>)
这里的相等是指:要么它们的值都为null,要么它们都含有基础类型的值且值相等。
相等和不相等操作符两端的可空类型要求是同一类型,或者可以进行类型转换。例如下面的代码是不合法的,因为可空类型int?和DateTime?之间不存在类型转换,即使它们都是null:
int? i = null; DateTime? dt = null; Console.WriteLine(i==dt);//error
2.3 类型转换
可空类型的类型转换进行遵循下面5条转换规则:
2.3.1 转换规则一
第一条也是最根本的一条类型转换规则是:
任何可空类型都可以显式地转换到它所对应的基础类型,而基础类型则可以隐式地转换到对应的可空类型。
下面的转换都是合法的:
int i =10; int? j = i;//隐式转换 j=20; int k = (int) j ;//显式转换
隐式转换总是安全的,而显式转换有可能失败。从可空类型向对应基础类型进行显式转换时,如果可空类型的值为null,转换就会失败,程序将引发一个异常。
这种可空类型与基础类型之间的转换是通过泛型结构NullableType<T>中所重载的类型转换操作符来实现的,其原型分别为:
public static explicit operator T(NullableType<T>) public static implicit operator NullableType<T>(T)
2.3.2 转换规则二
从转换规则一的第一部分出发可以得到第二条转换规则:
从可空类型到非可空类型只可能存在显式转换,能够进行转换的条件是:存在从可空类型对应的基础类型到非可空类型的隐式或显式转换。
下面的转换都是正确的:
int i = 5; short s = (short)i;//存在从int到short的显式转换 long l=(long) i;//存在从int到long的隐式转换
这种显式转换同样可能失败,失败的原因可能是可空类型的值为null,也有可能是可空类型的基础类型到非可空类型的显式转换失败。
下面的转换规则是错误的:
int i=5; long l=i;/error:从int?到long只能进行显式转换 DateTime dt = l;//error:从int到DateTime不存在类型转换
不过这条规则存在一个惟一的例外,即从任何可空类型都可以隐式转换到object类型,这是装箱转换在可空类型上的扩展,例如:
int? i =5; object obj = i;
2.3.3 转换规则三
从转换规则一的第二部分出发可以得到第三条转换规则:
从非可空类型到可空类型可能存在隐式或显式转换,能够进行转换的条件是:存在从非可空类型到可空类型对应的基础类型的陷式或显式转换。
下面的转换都是正确的:
short s=5; long l = 1000; int? i =s;//存在从short到int的隐式转换 i = (int?)l;//存在从long到int显式转换
上面最后一行代码可以看成是两次显式转换的组合:
i = (int?)((int)l);
而下面的转换是错误的:
long l=1000; int? i = l;//error,从long到int只能进行显式转换 DateTime dt = DateTime.Now; int? j = dt;//error:从DateTime到int不存在类型转换
当被转换的类型是object类型时,该转换规则可以被视为拆箱转换在可空类型上的扩展,例如下面的转换是允许的:
object obj = null; int? i = (int?)obj;
2.3.4 转换规则四
上面三条转换规则结合起来就可以得到第四条转换规则:
从可空类型到其它可空类型可能存在隐式或显示转换,且能够转换的条件是:在两个可空类型的基础类型之间存在隐式或显式转换。
下面的转换都是正确的:
int? i=5; double? d=3.14; d=i;//存在从int到double的隐式转换 i=(int?)d;//存在从double到int的显式转换 //上面最后一行代码可以看成是3次显式转换的组合 i=(int?)((int)((double)d);
而下面的转换是错误的:
double?d = 3.14; int i=d;//error:从double到int只能进行显式转换 int? i=d; DateTime? dt=DateTime.Now; int? j= (int?)dt;//error:不存在从DateTime到int的类型转换
2.3.5 转换规则五
常量值null可以被隐式转换为任何可空类型。在将空值null赋给可空类型的变量时,其实就隐含地进行了这种转换,例如下面的转换都是正确的:
int? i=null; DateTime? dt = null;
因此常量值null也可以出现在任何可空类型的相等和不等比较表达式中,例如:
int? i = null; Console.WriteLine(i=null); //输出True DateTime? dt = DateTime.Now; Console.WriteLine(dt!=null);//输出True
但是该转换规则不适用于任何值为null的变量,例如下面的的代码都是错误的,因为虽然object类型的变量obj的值也是空值,但它不是一个常量:
object obj = null; int?i = null; double? d = obj;//error Console.WriteLine(obj==i);//error
2.3 操作符提升
泛型结构NullableType<T>只重载了4个操作符(相等、不等、显式类型转换、隐式类型转换),但如果仅因此就导致其它操作符不能作用于可空类型的话,引入可空类型的意义就大打折扣了。例如,让加、减、乘、除这样的操作符作用于可空类型int?应当是顺理成章的事:
int? x=2; int? y=3; int? z=x+y;//z=5
这种功能的实现是通过一种名叫“操作符提升”的机制实现的。操作符提升指的是:
如果某个操作符能够作用于某个值类型,那么它同样能够作用于该类型所对应的可空类型。即操作符的作用范围从基础类型“提升”到了可空类型。
提升后的操作符作用于可空类型时,如果表达式中的所有操作数都不含空值,那么操作符的作用效果和没有提升之前是一样的,只不过最后将基础类型隐式地转换为可空类型。上面最后一行代码的执行效果可以视为:
int? z=(int?)(x.Value + y.Value);
而当表达式中出现了值为null的操作数时,操作符的作用效果就略为复杂一些。这时可以分为以下3种情况进行讨论:
--对于一元操作符“+”、“-”、“++”、“--”、“!”和“~”,表达式的返回类型和操作数相同,且返回值为null。
--对于二元的算术操作符“+”、“-”、“*”、“/”、“%”,以及位操作符“&”、“|”,“^"、“<<”、“>>”,表达式的返回类型和两个操作数的类型都相同,且返回值为null。
--对于关系操作符“<”、“>”、“<=”、“>=”,表达式的返回类型为布尔类型,且返回值为false。
例如,设x、y、z是3个可空类型的int?的变量,那么表达式z=x+y的运算效果可以用下面的等价代码来表示:
if(x.HasValue && y.HasValue) z = x.Value + y.Value; else z = null;
下面的程序定义了一个结构ComplexNumber来封装对复数的操作,该结构的两个私有字段m_value1和m_value2均为可空类型double?,而在程序的主方法中又分别使用到了结构ComplexNumber及其可空类型ComplexNumber?的变量:
class NullableOperatorLiftSample { static void Main() { ComplexNumber c1 = new ComplexNumber(); ComplexNumber? c2 = null; ComplexNumber.ShowOperarion(c1, c2); Console.WriteLine(); c2 = new ComplexNumber?(c1); ComplexNumber.ShowOperarion(c1, c2); Console.WriteLine(); c1.Value1 = 3; c1.Value2 = 6; c2 = new ComplexNumber(0, 0); ComplexNumber.ShowOperarion(c1, c2); Console.WriteLine(); c2 = new ComplexNumber(4, 8); ComplexNumber.ShowOperarion(c1, c2); } } /// <summary> /// 结构:复数定义 /// </summary> public struct ComplexNumber { //字段 private double? m_value1; private double? m_value2; //属性 public double? Value1 { get { return m_value1; } set { m_value1 = value; } } public double? Value2 { get { return m_value2; } set { m_value2 = value; } } //构造函数 public ComplexNumber(double? dValue1, double? dValue2) { m_value1 = dValue1; m_value2 = dValue2; } //操作符重载 public static ComplexNumber operator +(ComplexNumber c1, ComplexNumber c2) { return new ComplexNumber(c1.m_value1 + c2.m_value1, c1.m_value2 + c2.m_value2); } public static ComplexNumber operator -(ComplexNumber c1, ComplexNumber c2) { return new ComplexNumber(c1.m_value1 - c2.m_value1, c1.m_value2 - c2.m_value2); } public static ComplexNumber operator *(ComplexNumber c1, ComplexNumber c2) { double? v1 = c1.m_value1 * c2.m_value1 - c1.m_value2 * c2.m_value2; double? v2 = c1.m_value1 * c2.m_value1 + c1.m_value2 * c2.m_value2; return new ComplexNumber(v1,v2); } public static ComplexNumber operator /(ComplexNumber c1, ComplexNumber c2) { double? den = c2.m_value1 * c2.m_value1 + c2.m_value2 * c2.m_value2; double? v1 = (c1.m_value1 * c2.m_value1 + c1.m_value2 * c2.m_value2) / den; double? v2 = (c1.m_value1 * c2.m_value2 - c1.m_value1 * c2.m_value2) / den; return new ComplexNumber(v1,v2); } //方法 public override string ToString() { string str1 = m_value1.HasValue ? m_value1.ToString() : "Undefined"; string str2 = m_value2.HasValue ? m_value2.ToString() : "Undefined"; return string.Format("{0} + {1}i", str1, str2); } public static void ShowOperarion(ComplexNumber? c1, ComplexNumber? c2) { string str1 = c1.HasValue ? c1.Value.ToString() : "null"; string str2 = c2.HasValue ? c2.Value.ToString() : "null"; ComplexNumber? c3 = c1 + c2; string str3 = c3.HasValue ? c3.Value.ToString() : "null"; Console.WriteLine("<{0}> + <{1}> = <{2}>", str1, str2, str3); c3 = c1 - c2; str3 = c3.HasValue ? c3.Value.ToString() : "null"; Console.WriteLine("<{0}> - <{1}> = <{2}>", str1, str2, str3); c3 = c1 * c2; str3 = c3.HasValue ? c3.Value.ToString() : "null"; Console.WriteLine("<{0}> * <{1}> = <{2}>", str1, str2, str3); c3 = c1 / c2; str3 = c3.HasValue ? c3.Value.ToString() : "null"; Console.WriteLine("<{0}> / <{1}> = <{2}>", str1, str2, str3); } }
结构ComplexNumber定义的重载方法ToString用于输出复数的数学书写形式,而静态方法ShowOperation则用于输出两个可空类型ComplexNumber?的变量进行四则运算结果。由于空值null直接输出时没有内容,这两个方法均将其转换为字符串null后输出。定义的操作符重载代码中,有二元操作符“加、减、乘、除”对可空类型double?的提升运算,而程序主方法的代码中又有这些操作符对可空类型ComplexNumber?的提升运算。输出结果为:
<Undefined + Undefinedi> + <null> = <null> <Undefined + Undefinedi> - <null> = <null> <Undefined + Undefinedi> * <null> = <null> <Undefined + Undefinedi> / <null> = <null> <Undefined + Undefinedi> + <Undefined + Undefinedi> = <Undefined + Undefinedi> <Undefined + Undefinedi> - <Undefined + Undefinedi> = <Undefined + Undefinedi> <Undefined + Undefinedi> * <Undefined + Undefinedi> = <Undefined + Undefinedi> <Undefined + Undefinedi> / <Undefined + Undefinedi> = <Undefined + Undefinedi> <3 + 6i> + <0 + 0i> = <3 + 6i> <3 + 6i> - <0 + 0i> = <3 + 6i> <3 + 6i> * <0 + 0i> = <0 + 0i> <3 + 6i> / <0 + 0i> = <非数字 + 非数字i> <3 + 6i> + <4 + 8i> = <7 + 14i> <3 + 6i> - <4 + 8i> = <-1 + -2i> <3 + 6i> * <4 + 8i> = <-36 + 60i> <3 + 6i> / <4 + 8i> = <0.75 + 0i> 请按任意键继续. . .
2.4 可空布尔类型
布尔类型所对应的可空类型bool? 非常特殊,它是唯一不符合操作符提升规则的可空类型。能够作用于布尔类型的操作符有位操作符“&”和“|”,以及条件逻辑操作符“&&”、“||”和“!”。而这些操作符作用于可空类型bool?的操作数时,只有逻辑非操作符“!”符合提升规则。而对其它的操作符,如果某个操作数的值为null,表达式的返回值并不一定为null。实际的情况是:
--对于与操作符“&”,只要有一个操作数的值为false,表达式的值就为false;只要有一个操作数的值为true,表达式的值就是另一个操作数的值;两个操作数都为null时,表达式的值为null。
--对于或操作符“|”,只要有一个操作数的值为true,表达式的值就为true;只要有一个操作数的值为false;表达式的值就是另一个操作数的值;两个操作数都为null时,表达式的值为null。
--对于逻辑与操作符“&&”,其运算效果与操作符“&”相同;对于逻辑或操作符“||”、其运算效果和或操作符“|”相同。
之所以采用这种规则,而不和.NET类型中的其它可空类型保持一致,完全是为了保持布尔代数中的运算法则,而这些法则在计算机世界中早已根深蒂固,并在逻辑电路、数据库领域有着广泛的应用。例如在Transact-SQL标准中定义的布尔类型就包含true、false和null3个值,其运算法则也和上面所说相同。
第三章中介绍过的条件逻辑表达式的“短路”效应在引入可空类型之后仍然适用。
class NullableBooleanSample { static void Main() { BooleanClass b1 = new BooleanClass(null); BooleanClass b2 = new BooleanClass(null); for (int i = 0; i < 3; i++) { Console.WriteLine("!{0} = {1}", b1, new BooleanClass(!b1.Value)); for (int j = 0; j < 3; j++) { Console.Write(" {0} && {1} = {2} ", b1, b2, new BooleanClass(b1.Value & b2.Value)); Console.WriteLine(" {0} || {1} = {2}", b1, b2, new BooleanClass(b1.Value | b2.Value)); b2++; } b1++; } } } /// <summary> /// 类:可空布尔类型 /// </summary> public class BooleanClass { //字段 private bool? m_value; //属性 public bool? Value { get { return m_value; } } //构造函数 public BooleanClass(bool? bValue) { m_value = bValue; } //方法 public static BooleanClass operator ++(BooleanClass bc) { bool? newValue = null; if (bc.m_value == null) newValue = false; else if (bc.m_value == false) newValue = true; return new BooleanClass(newValue); } public override string ToString() { if (m_value == null) return " null"; else if((bool)m_value) return " true"; else return "false"; } }
输出结果:
! null = null null && null = null null || null = null null && false = false null || false = null null && true = null null || true = true !false = true false && null = false false || null = null false && false = false false || false = false false && true = false false || true = true ! true = false true && null = null true || null = true true && false = false true || false = true true && true = true true || true = true 请按任意键继续. . .
5 空值结合操作符
可空类型引入了一个新的双问号操作符"??",也叫做空值结合操作符。该操作符的左侧为一个可空类型的表达式,右侧为一个该可空类型对应的基础类型的表达式。空值结合操作符的运算规则是:如果左侧表达式的值为null,那么返回右侧表达式的值;否则返回左侧表达式的值。例如:
int? x = null; int y=3; int z=x ?? y;//z=3 x=2; z=x?? y;//z=2;
也就是说,如果变量x的类型为NullableType<T>,变量y和z的类型均为T,那么表达式z=x??y的求值过程可用下面的等效代码来表示:
if(x.HasValue) z = x.Value; else z = y;
6 小结
C#语言中的任何一种值类型都可以作为基础类型而扩展出一个新的可空类型,可空类型取值可以是基础类型的某个值,也可以是空值null。
可空类型的功能是借助于泛型来实现的。将泛型结构NullableType<T>的类型参数替换为一个值类型,得到的构造类型就是该值类型对应的可空类型。从基础类型向可空类型转换可以是隐式的,而从可空类型向基础类型转换必须是显式的。基础类型上的运算也可以通过操作符提升扩展到可空类型。