首页 > 学院 > 开发设计 > 正文

[CLR via C#]19. 可空值类型

2019-11-17 03:19:10
字体:
来源:转载
供稿:网友

[CLR via C#]19. 可空值类型

  我们知道,一个值类型的变量永远不可能为null。它总是包含值类型本身。遗憾的是,这在某些情况下会成为问题。例如,设计一个数据库时,可将一个列定义成为一个32位的整数,并映射到FCL的Int32数据类型。但是,数据库中的一个列可能允许值为空;用Microsoft .NET Framework处理数据库可能变得相当困难,因为在CLR中,没有办法将一个Int32值表示为null。   Microsoft ADO.NET的表适配器确实支持可空类型。但遗憾的是,System.Data.SqlType命名空间中的值类型没有用可空类型替换,部分原因是类型之间没有"一对一"的对应关系。例如,SqlDecimal类型对最大允许38位数,而普通的Decimal类型最大只允许29位数。   还有一个例子:在java中,java.util.Date类是一个引用类型,所以该类型的一个变量能设为null。但在CLR中,System.DateTime是一个值类型,一个DateTime变量永远都不能设为null。如果用java写的一个应用程序想和运行CLR的一个web服务交流日期/时间,那么一旦java发送了一个null,就会出问题,因为CLR不知道如何表示null,不知道如何操作它。   为了解决这个问题,Microsoft在CLR中引入了可空值类型(nullable value type)的概念。为了理解它们是如何工作的,先看一看System.Nullable<T>类。他是在FCL中定义的。以下是System.Nullable<T>类型定义的逻辑表示:

[Serializable][DebuggerStepThrough]public struct Nullable<T> where T : struct{  #region Sync with runtime code  //下面两个字段表示状态  internal T value;  internal bool has_value;  #endregion  public Nullable(T value)  {    this.has_value = true;    this.value = value;  }   public bool HasValue  {    get { return has_value; }  }  public T Value  {   get    {    if (!has_value)    throw new InvalidOperationException("Nullable object must have a value.");    return value;    }  }  public override bool Equals(object other)  {    if (other == null)    return has_value == false;    if (!(other is Nullable<T>))    return false;    return Equals((Nullable<T>)other);  }  bool Equals(Nullable<T> other)  {    if (other.has_value != has_value)    return false;    if (has_value == false)    return true;    return other.value.Equals(value);  }  public override int GetHashCode()  {    if (!has_value)    return 0;    return value.GetHashCode();  }  public T GetValueOrDefault()  {    return value;  }  public T GetValueOrDefault(T defaultValue)  {    return has_value ? value : defaultValue;  }  public override string ToString()  {    if (has_value)    return value.ToString();    else    return String.Empty;  }  public static implicit operator Nullable<T>(T value)  {    return new Nullable<T>(value);  }  public static explicit operator T(Nullable<T> value)  {    return value.Value;  }  //  // These are called by the JIT  //  #PRagma warning disable 169  //  // JIT implementation of box valuetype System.Nullable`1<T>  //  static object Box(T? o)  {    if (!o.has_value)    return null;    return o.value;  }  static T? Unbox(object o)  {    if (o == null)    return null;    return (T)o;  }  #pragma warning restore 169}

  可以看出,这个类封装了也可以为null的一个值类型的表示。由于Nullable<T>本身是一个值类型,所以它的实例仍然是"轻量级"的。也就是说,实例仍然在栈上,而且一个实例的大小就是原始值类型的大小加上一个Boolean字段的大小。注意,Nullable的类型参数T被约束为struct。这是由于引用类型的变量已经可以为null,所以没必要再去照顾它。.

  现在,如果想要在代码中使用一个可空的Int32,就可以向下面这样写:

Nullable<Int32> x = 5;Nullable<Int32> 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=5y: HasValue=False, Value=0   一、C#对可空值类型的支持   注意,C#允许在代码中使用简单的语法来初始化上述两个Nullable<Int32>变量x和y。事实上,C#开发团队希望将可空值类型集成到C#语言中,是它们成为"一等公民"。为此,C#提供了一个更清晰的语法来处理可空值类型。C#允许用问号表示法来声明并初始化x和y变量:

Int32? x =5;Int32? y =null;

  在C#中,Int32等价于Nullable<Int32>。但是,C#在此基础上更进一步,允许开发人员在可空实例上进行转换和转型。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)}

  C#还允许向可空实例应用操作符。下面是一些例子:

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 */ }}

  下面总结了C#如何解析操作符:

  * 一元操作符 操作符是null,结果也是null   * 二元操作符 两个操作符中任何一个是null,结果就是null。但有一个例外,它发生在将&和|操作符应用于Boolean?操作数的时候。在这种情况下,这两个操作符的行为和SQL的三值逻辑一样的。对于这两个操作符,如果两个操作符都不是null,那么操作符和平常一样工作。如果两个操作符都是null,结果就是null。特殊情况就是其中之一为null时发生。下面列出了针对操作符的各种true,false和null组合:

操作符→truefalsenull
操作符↓
true& = true| = true& = false| = true& = null| = true
false

& = false

| = true

& = false| = false& = false| = null
null& = null| = true& = false| = null& = null| = 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());}

  19.2 C#的空结合操作符  C#提供了一个所谓的"空结合操作符",即??操作符,它要获取两个操作符。假如左边的操作符不为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";}

  有人争辩说??操作符不过是?:操作符的"语法糖"而已,所以C#团队不应该将这个操作符添加到语言中。实际上,??提供了重大的语法上的改进。

  第一个改进是??操作符能更好的支持表达式:

Func<String> f = () => SomeMethod ?? "Untitled";

  相比下一行代码,上述代码更容易容易阅读和理解。下面这行代码要求进行变量赋值,而且用一个语句还搞不定:

Func<String> f = () => { var temp = SomeMethod(); return temp !=null ?temp : "Untitled"; }

  第二个改进就是??在符合情形下更好用。例如,下面这行代码:

String s = SomeMethod() ?? SomeMethod2 ?? "Untitled";

  它比下面这一堆代码更容易理解和阅读:

String s;var sm1 = SomeMethod();if (sm1 != null) s = sm1;else {var sm2 = SomeMethod2();if (sm2 !=null) s = sm2;elses = "Untitled";}

  三、CLR对可空值类型的特殊支持   1.可空值类型的装箱   先假定有一个为null的Nullable<Int32>变量。如果将该变量传给一个期待获取一个Object的方法,那么该变量必须装箱,并将对已装箱的Nullable<Int32>的一个引用传给方法。但是,这在逻辑上讲不通,因为现在向方法传递的一个非null的值——而Nullable<Int32>变量逻辑上包含的是null值。为了解决这个问题,CLR会在装箱一个可空变量时执行一些特殊代码。   具体地说,当CLR对一个Nullable<T>实例进行装箱时,会检查它是否为null。如果是CLR不实际装箱任何内容,并返回null。如果可空类型实例不为null,CLR从可空实例中取出值,并对其进行装箱。也就是说,一个值为5的Nullable<Int32>会装箱成为值为5的一个已装箱的Int32。以下代码对这一行进行了演示:

private static void Boxing() {    // 对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;}

  2. 可空值类型的拆箱   CLR允许将一个已装箱的值类型T拆箱为一个T或者一个Nullable<T>。如果对已装箱值类型的引用是null,而且要把它拆箱为一个Nullable<T>,那么CLR会将Nullable<T>的值设为null。以下代码进行了演示:

private static void Unboxing() {    // 创建一个已装箱的In
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表