可空值类型
我们知道值类型的变量永远不会为null
;它总是包含值类型的值本身. 这正是值类型一词的由来.
有些情况下会成为问题: 例如在设计数据库时,可以将一列数据类型定义为32位整数,并映射到FCL中的Int32数据类型, 但是数据库中的一列可能允许值为空,这样在CLR中就没办法将Int32值表示为null.
另一个例子:Java的java.util.Date类是引用类型,所以该类型的变量能设为null. 但是CLR的System.DateTime是值类型,无法设为null.
为了解决这个问题.CLR中引入了 可空值类型 的概念. System.Nullable<T>
// 约束T为结构, 因为引用类型本身可以为null
public struct Nullable<T> where T : struct
{
// 这两个字段表示状态
private bool hasValue = false; // 假定null
internal T value = default(T); // 假定所有位都为0
...
}
// 要在代码中使用一个可空的Int32类型,可以这么写
Nullable<Int32> x = 5;
Nullable<Int32> y = null;
Console.WriteLine($"x: HasValue={x.HasValue}, Value={x.Value}");
Console.WriteLine($"y: HasValue={y.HasValue}, Value={y.GetValueOrDefault()}");
// 以下代码会报异常: Nullable object must have a value.
// Console.WriteLine($"{y.Value}");
// x: HasValue=True, Value=5
// y: HasValue=False, Value=0
C#对可空值类型的支持
C# 允许使用相当简单的语法初始化上述两个Nullable<Int32>
变量x和y.
Int32? x = 5;
Int32? y = null;
在C#中Int32?
等价于Nullable<Int32>
,C#在此基础上更进一步,允许开发人员在可空实例上执行转换和转型.
private static void ConversionsAndCasting()
{
// 从非可空的 Int32 转换为 Nullable<Int32>
Int32? a = 5;
// 从'null'隐式转换为 Nullable<Int32>
Int32? b = null;
// 从 Nullable<Int32> 显示转换为 Int32
Int32 c = (Int32)a;
// 在可空基类型之间的转型
Double? d = 5; // Int32 转型到 Double? (d是double类型 值为5)
Double? e = b; // Int32? 转型到 Double? (e为 null)
}
还允许向可空实例应用操作符:
private static void Operators()
{
Int32? a = 5;
Int32? b = null;
// 一元操作符 (+ ++ - -- ! ~)
a++; // a = 6
b = -b; // b = null
// 二元操作符 (+ - * / % & | ^ << >>)
a = a + 3; // a = 9
b = b * 3; // b = null;
// 相等性操作符 (== !=)
if (a == null) { /* no */ } else { /* yes */ }
if (b == null) { /* yes */ } else { /* no */ }
if (a != b) { /* yes */ } else { /* no */ }
// 比较操作符 (<, >, <=, >=)
if (a < b) { /* no */ } else { /* yes */ }
}
- 一元操作符 (+,++,-,–,!,~)
- 操作符是null,结果也是null
- 二元操作符 (+,-,* , /, %,&,|,^,<<,>>)
- 两个操作数任何一个是null, 结果就是null
- 但是有一个例外: 发生在 将
&
和|
操作符应用于Boolean?
操作数的时候 - 对于这两个操作符,如果两个操作符都不是null,那么操作符和平常一样工作。如果两个操作符都是null,结果就是null。
- 特殊情况就是其中之一为null时发生。下面列出了针对操作符的各种true,false和null组合:
- 相等性操作(== , !=)
- 两个操作符都是null,两者相等。一个操作符为null,则两个不相等。两个操作数都不是null,就比较值来判断是否相等。
- 关系操作符(<,>,<=,>=)
- 两个操作符任何一个是null,结果就是false。两个操作数都不是null,就比较值。
应该注意的是,操作符实例时会生成大量代码。例如以下方法:
private static Int32? NullableCodeSize(Int32? a, Int32? b)
{
return (a + b);
}
在编译上述方法时,会生成相当多的IL代码,而且会使对可空类型
的操作符慢于非可空类型
执行的同样的操作。编译器生成的代码等价于以下C#代码:
private static Nullable<Int32> NullableCodeSize(
Nullable<Int32> a, Nullable<Int32> b)
{
Nullable<Int32> nullable1 = a;
Nullable<Int32> nullable2 = b;
if (!(nullable1.HasValue & nullable2.HasValue))
{
return new Nullable<Int32>();
}
return new Nullable<Int32>(nullable1.GetValueOrDefault() + nullable2.GetValueOrDefault());
}
重载操作符例子:
public static void Main()
{
Point? p1 = new Point(1,1);
Point? p2 = new Point(2,2);
Console.WriteLine($"是否相等? {(p1 == p2).ToString()}");
Console.WriteLine($"是否不相等?{(p1 != p2).ToString()}");
//是否相等? False
//是否不相等? True
}
internal struct Point
{
private Int32 m_x, m_y;
public Point(Int32 x, Int32 y)
{
m_x = x;
m_y = y;
}
public static Boolean operator==(Point p1, Point p2)
{
return (p1.m_x == p2.m_x) && (p1.m_y == p2.m_y);
}
public static bool operator !=(Point p1, Point p2)
{
return !(p1 == p2);
}
}
C#的空接合操作符
C#提供了一个 空接合操作符
(null-coalescing operator) 即 ??
操作符, 它要获取两个操作数, 假如左边的操作数不为null,就返回左边这个操作数的值. 左边操作数为null,就返回右边的操作数的值.
利用空接合操作符
,可以方便地设置变量的默认值.
此操作符的一个好处在于,它既能用于引用类型
,也能用于可空值类型
.
private static void NullCoalescingOperator()
{
Int32? b = null;
// 下面这行等价于:
// x = (b.HasValue) ? b.Value : 123
Int32 x = b ?? 123;
Console.WriteLine(x); // "123"
// 下面这行等价于:
// String temp = GetFilename();
// filename = (temp != null) ? temp : "Untitled";
String filename = GetFilename() ?? "Untitled";
}
关于??
与?:
的说明: 虽然功能相似, 但是??
提供了重大的语法上的改进.
??
能更好的支持表达式
// 更容易阅读和理解
Func<String> f = () => SomeMethod ?? "Untitled";
// 要求进行变量的赋值,无法用一个语句完成
Func<String> f = () =>
{
var temp = SomeMethod();
return temp !=null ? temp : "Untitled";
};
??
能在复合情形下更好用
// 如果SomeMethod()为null,且SomeMethod2为null的前提下,s = "Untitled";
// 从左到右顺序, 最右边是默认值
String s = SomeMethod() ?? SomeMethod2() ?? "Untitled";
String s;
var sm1 = SomeMethod();
// 先判断SomeMethod()是否为null,不为null,则s等于这个值.
if (sm1 != null)
s = sm1;
else
{
var sm2 = SomeMethod2();
if (sm2 != null)
s = sm2;
else
s = "Untitled";
}
CLR对可空值类型的特殊支持
CLR内建对可空值类型的支持. 这个特殊的支持是针对
- 装箱
- 拆箱
- 调用GetType
- 调用方法
使可空值类型能无缝地集成到CLR中.
可空值类型的装箱
先假定有一个为null
的Nullable<Int32>变量
。如果将该变量传给一个期待获取一个Object
的方法,那么该变量必须装箱,并将对已装箱的Nullable<Int32>
的引用
传给方法。但对表面上为null
的值进行装箱不符合直觉,即使Nullable
具体地说, 当CLR对Nullable<T>
实例进行装箱时,会检查它是否为null.
- 如果是: CLR不装箱任何东西,直接返回null
- 如果不为null: CLR从可空实例中取出值并进行装箱.
- 也就是说, 一个值为5的
Nullable<T>
会装箱成值为5的已装箱Int32
- 也就是说, 一个值为5的
// 对Nullable<T>进行装箱,要么返回null,要么返回一个已装箱的T
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's type={0}", o.GetType()); // "System.Int32"
// 其实在第一节中的Nullable<T>源码中已有显示,如:
static object Box(T? o)
{
if (!o.has_value)
return null;
return o.value;
}
可空值类型的拆箱
CLR运行将已装箱的值类型T
拆箱为一个T
或者Nullable<T>
- 如果对
已装箱值类型
的引用是null
, 并且要拆箱为一个Nullable<T>
- 那么CLR会将
Nullable<T>
设为null.
// 创建一个已装箱的Int32
Object o = 5;
// 把它拆箱为一个 Nullable<Int32> 和一个 Int32
Int32? a = (Int32?)o; // a = 5
Int32 b = (Int32)o; // b = 5
// 创建初始化为null的一个引用
o = null;
// 把它"拆箱"为一个Nullable<Int32> 和一个 Int32
a = (Int32?)o; // a = null
b = (Int32) o; // NullReferenceException
// 同样的,在第一节中的Nullable<T>源码中已有显示,如:
static T? Unbox(object o)
{
if (o == null)
return null;
return (T)o;
}
通过可空值类型调用GetType
在一个Nullable<T>
对象上调用GetType
时,CLR实际上会”撒谎”说类型是T
,而不是Nullable<T>
。以下代码演示了这一行为:
Int32? x = 5;
// 下面会显示"System.Int32"而不是"System.Nullable<Int32>"
Console.WriteLine(x.GetType());
Int32? x = null;
// 空指针异常 NullReferenceException
// Console.WriteLine(x.GetType());
通过可空值类型调用接口方法
在下面代码中,将一个Nullable<Int32>
类型的变量n
转型为一个接口类型IComparable<Int32>
。然而,Nullable<T>
不像Int32
那样实现了IComparable<Int32>
接口。C#编译器允许这样的代码通过编译,而且CLR的校验器也会认为这样的代码是可验证的,从而允许我们使用一种更简洁的语法:
Int32? n = 5;
Int32 result = ((IComparable<Int32>) n).CompareTo(5); // 能顺利通过编译和允许
Console.WriteLine(result); // 0
// 假如CLR没有提供这一特殊支持,那么为了在一个可空值类型上调用接口方法,就要写非常繁琐的代码。
// 首先必须转型对已拆箱的值类型,
// 然后才能转型为接口以发出调用:
result = ((IComparable)(Int32) n).CompareTo(5); // 这太繁琐了
Console.WriteLine(result); // 0