我们知道,一个值类型的变量永远不可能为null。它总是包含值类型本身。遗憾的是,这在某些情况下会成为问题。例如,设计一个数据库时,可将一个列定义成为一个32位的整数,并映射到FCL的Int32数据类型。但是,数据库中的一个列可能允许值为空;用Microsoft .NET Framework处理数据库可能变得相当困难,因为在CLR中,没有办法将一个Int32值表示为null。

  还有一个例子:在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 ;     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 = ;
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=
y: HasValue=False, Value=

C#对可空值类型的支持

  C#允许使用相当简单的语法初始化上述两个Nullable<Int32>变量x和y。事实上,c#开发团队的目的是将可空类型集成到c#语言中,使之成为“一等公民“。为此,c#提供了更清晰的语法来处理可空值类型。C#允许用问号表示法来声明并初始化x和y变量:

Int32? x =;
Int32? y =null;

在C#中,Int32等价于Nullable<Int32>。但是,C#在此基础上更进一步,允许开发人员在可空实例上进行转换和转型。C#还允许开发人员向可空实例类型应用操作符。以下代码对此进行了演示;

private static void ConversionsAndCasting()
{
// 从非可空的 Int32 转换为 Nullable<Int32>
Int32? a = ;
// 从'null'隐式转换为 Nullable<Int32>
Int32? b = null;
// 从 Nullable<Int32> 显示转换为 Int32
Int32 c = (Int32)a;
// 在可空基类型之间的转型
Double? d = ; // Int32 转型到 Double? (d是double类型 值为5)
Double? e = b; // Int32? 转型到 Double? (e为 null)
}

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

private static void Operators()
{
Int32? a = ;
Int32? b = null; // 一元操作符 (+ ++ - -- ! ~)
a++; // a = 6
b = -b; // b = null // 二元操作符 (+ - * / % & | ^ << >>)
a = a + ; // a = 9
b = b * ; // 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组合:

相等性操作

两个操作符都是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());
}

C#的空接合操作符

  C#提供了一个所谓的"空结合操作符",即??操作符,它要获取两个操作符。假如左边的操作符不为null,就返回操作符这个操作符的值。如果左边的操作符为null,就返回右边的操作符的值。利用空接合操作符,可方便地设置的默认值。

  空接合操作符的一个妙处在于,它既能用于引用类型也能用于可空值类型。以下代码演示了如何使用??操作符:

private static void NullCoalescingOperator() {
Int32? b = null; // 下面这行等价于:
// x = (b.HasValue) ? b.Value : 123
Int32 x = b ?? ;
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;
else
s = "Untitled";
}

clr对可空值类型的特殊支持

clr内建对可空类型的支持。这个特殊的支持是针对装箱、拆箱、调用getType和调用接口方法提供的,它使可控类型能无缝地集成到clr中,而且使它们具有更自然的行为,更符合大多数开发人员的预期。

可空值类型的装箱

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

  具体地说,当CLR对一个Nullable<T>实例进行装箱时,会检查它是否为null。如果是CLR不实际装箱任何内容,直接返回null。如果可空类型实例不为null,CLR从可空实例中取出值,并对其进行装箱。也就是说,一个值为5的Nullable<Int32>会装箱成为值为5的一个已装箱的Int32。以下代码对这一行进行了演示:

// 对Nullable<T>进行装箱,要么返回null,要么返回一个已装箱的T
Int32? n = null;
Object o = n; // o 为 null
Console.WriteLine("o is null={0}", o == null); // "True" n = ;
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 = ; // 把它拆箱为一个 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 = ;
// 下面会显示"System.Int32"而不是"System.Nullable<Int32>"
Console.WriteLine(x.GetType());

如果设为null,会报错

Int32? x = null;
// 下面会显示"System.Int32"而不是"System.Nullable<Int32>"
Console.WriteLine(x.GetType());

输出结果

Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.

at System.Object.GetType()

at AttributeStudy2.Program.Main() in E:\project\2Core\Test\ClrStudy\AttributeStudy2\Program.cs:line 19

通过可空值类型调用接口方法

  在下面代码中,将一个Nullable<Int32>类型的变量n转型为一个接口类型IComparable<Int32>。然而,Nullable<T>不像Int32那样实现了IComparable<Int32>接口。C#编译器允许这样的代码通过编译,而且CLR的校验器也会认为这样的代码是可验证的,从而允许我们使用一种更简洁的语法:

Int32? n = ;
Int32 result = ((IComparable<Int32>)n).CompareTo(); // 能顺利通过编译和允许
Console.WriteLine(result); //

假如CLR没有提供这一特殊支持,那么为了在一个可空值类型上调用接口方法,就要写非常繁琐的代码。首先必须转型对已拆箱的值类型,然后才能转型为接口以发出调用:

result = ((IComparable)(Int32)n).CompareTo(); // 这太繁琐了
Console.WriteLine(result); //

重温CLR(十四) 可空类型的更多相关文章

  1. 重温CLR(十一) 枚举类型、位标志和数组

    枚举类型 枚举类型(enumerated types)定义了一组"符号名称/值"配对.例如,以下Color类型定义了一组符号,每个符号都标识一种颜色: internal enum ...

  2. 重温CLR(四)基元类型、引用类型、值类型

    编程语言的基元类型 编译器直接支持的数据类型称为基元类型(primitive type).基元类型直接映射到framework类型(fcl)中存在的类型. 下表列出fcl类型 从另一个角度,可以认为C ...

  3. 《Two Dozen Short Lessons in Haskell》(二十四)代数类型

    这是<Two Dozen Short Lessons in Haskell>这本书的最后一章,第23章没有习题. 这一章里介绍了Haskell如果自定义一种类型,并且用一个双人博弈游戏为例 ...

  4. Scala入门到精通——第二十四节 高级类型 (三)

    作者:摆摆少年梦 视频地址:http://blog.csdn.net/wsscy2004/article/details/38440247 本节主要内容 Type Specialization Man ...

  5. Java编程思想之十四 类型信息

    第十四章 类型信息 运行时类型信息使得你可以在程序运行时发现和使用类型信息 14.1 为什么需要RTTI 面向对象编程中基本的目的是:让代码只操作对基类的引用. 多态: import java.uti ...

  6. thinkPHP 空模块和空操作、前置操作和后置操作 详细介绍(十四)

    原文:thinkPHP 空模块和空操作.前置操作和后置操作 详细介绍(十四) 本章节:介绍 TP 空模块和空操作.前置操作和后置操作 详细介绍 一.空模块和空操作 1.空操作 function _em ...

  7. 【转】四、可空类型Nullable<T>到底是什么鬼

    [转]四.可空类型Nullable<T>到底是什么鬼 值类型为什么不可以为空 首先我们都知道引用类型默认值都是null,而值类型的默认值都有非null. 为什么引用类型可以为空?因为引用类 ...

  8. 四、可空类型Nullable<T>到底是什么鬼

    值类型为什么不可以为空 首先我们都知道引用类型默认值都是null,而值类型的默认值都有非null. 为什么引用类型可以为空?因为引用类型变量都是保存一个对象的地址引用(就像一个url对应一个页面),而 ...

  9. 重温CLR(十五) 托管堆和垃圾回收

    本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存.简单地说,本章要解释clr中的垃圾回收期是如何工作的,还要解释相关的性能问题.另外,本章讨论了如何设计 ...

随机推荐

  1. javaScript tips —— 标签上的data属性

    HTML5规定可以为元素添加非标准型的属性,只需添加前缀data-,这些属性可以随意添加,随意命名,目的是为元素提供与渲染无关的信息,或提供语义信息. 传统获取方式 'getAttribute' da ...

  2. git branch 新建,推送与删除

    在开发的许多时候我们都需要使用git提供的分支管理功能. 1.新建本地分支:git checkout -b test  新建一个名为:test 的本地分支. 2.提交本地分支:git push ori ...

  3. 【Jmeter】压测mysql数据库中间件mycat

    背景 因为博主所负责测试的项目需要数据库有较大的吞吐量,在最近进行了升级,更新了一个数据库中间件 - - mycat.查询了一些资料,了解到这是阿里的一个开源项目,基于mysql,是针对磁盘的读与写, ...

  4. 【三小时学会Kubernetes!(零) 】系统结构及相关示例微服务介绍

    写在前面 牢牢占据容器技术统治地位的 Kubernetes,其重要性想必不言而喻,我保证本文是最详尽的 Kubernetes 技术文档,从我在后台排版了这么漫长的时间就能看出来.废话不多说 — — 以 ...

  5. [Vue]vue中各选项及钩子函数执行顺序

    在vue中,实例选项和钩子函数和{{}}表达式都是不需要手动调用就可以直接执行的. 一.生命周期图示 二.vue中各选项及钩子函数执行顺序 1.在页面首次加载执行顺序有如下: beforeCreate ...

  6. Java 基于javaMail的邮件发送(支持附件)

    基于JavaMail的Java邮件发送Author xiuhong.chen@hand-china.com Desc 简单邮件发送 Date 2017/12/8 项目中需要根据物料资质的状况实时给用户 ...

  7. C++ 知识点积累---待整理

  8. 引发事件代码封装成OnEventName

    引发事件的代码,通常可以封装成“On+事件名称”的方法(On:表示当“什么什么”的时候),如下所示: 1:引发事件代码: if (PropertyChanged != null)//为了实现将数据源的 ...

  9. 1-11 RHLE7-重定向和文件查找

    在Linux 系统中,一切皆设备Linux系统中使用文件来描述各种硬件,设备资源等例如:以前学过的硬盘和分区,光盘等设备文件sda1   sr0============================ ...

  10. oracle数据库简单的导入导出操作

    一.数据库导出 1.导出用户名/密码,,导出用户名为test_expdp.导出路径默认为oracle中的dpdump文件中 expdp test_expdp/test_expdp@orcl direc ...