可空值类型

我们知道值类型的变量永远不会为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";
}

关于???:的说明: 虽然功能相似, 但是??提供了重大的语法上的改进.

  1. ??能更好的支持表达式
// 更容易阅读和理解
Func<String> f = () => SomeMethod ?? "Untitled";

// 要求进行变量的赋值,无法用一个语句完成
Func<String> f = () =>
{
  var temp = SomeMethod();
  return temp !=null ? temp : "Untitled";
};
  1. ??能在复合情形下更好用
// 如果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中.

可空值类型的装箱

先假定有一个为nullNullable<Int32>变量。如果将该变量传给一个期待获取一个Object的方法,那么该变量必须装箱,并将对已装箱的Nullable<Int32>引用传给方法。但对表面上为null的值进行装箱不符合直觉,即使Nullable变量本身非null,它只是在逻辑上包含了null。为了解决这个问题,clr会在装箱可空变量时执行一些特殊代码,从表面上维持可空类型一等公民地位。

具体地说, 当CLR对Nullable<T>实例进行装箱时,会检查它是否为null.

  • 如果是: CLR不装箱任何东西,直接返回null
  • 如果不为null: CLR从可空实例中取出值并进行装箱.
    • 也就是说, 一个值为5的Nullable<T>会装箱成值为5的已装箱Int32
// 对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