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

为什么引用类型可以为空?因为引用类型变量都是保存一个对象的地址引用(就像一个url对应一个页面),而引用类型值为null的时候是变量值指向了一个空引用(如同一个空的url)

那为什么值不能有空值呢?其实很简单,因为如int值范围是-2147483648到2147483647。其中根本就没有给null值留那么一个位置。

我们为什么需要用到可空类型
举个栗子吧,我们定义一个人(Person),它有三个属性出生日期(BeginTime)、死亡日期(EndTime)、年龄(Age)。
如果这个人还健在人世,请问怎么给死亡日期赋值?有人很聪明说“为空啊”。是的,这就是我们的需求。
微软在C#2.0的时候就为我们引入了可null值类型( System.Nullable<T> ),那么下面来定义Person类。
public class Person
{
/// <summary>
/// 出生日期
/// </summary>
public DateTime BeginTime { get; set; }
/// <summary>
/// 死亡日期
/// </summary>
public System.Nullable<DateTime> EndTiem { get; set; }
public int Age
{
get
{
if (EndTiem.HasValue)//如果挂了(如果有值,证明死了)
{
return (EndTiem.Value - BeginTime).Days;
}
else//还没挂
{
return (DateTime.Now - BeginTime).Days;
}
}
}
}
这样,我们就可以很容易获得一个人的年龄了。
static void Main(string[] args)
{
Person p1 = new Person()
{
BeginTime = DateTime.Parse("1990-07-19")
}; Person p2 = new Person()
{
BeginTime = DateTime.Parse("1893-12-26"),
EndTiem = DateTime.Parse("1976-09-09")
}; Console.WriteLine("我今年" + p1.Age + "岁。");
Console.WriteLine("毛爷爷活了" + p2.Age + "岁。"); Console.ReadKey();
}
可空类型的实现
我们前面用到了 System.Nullable<DateTime> 来表示可空时间类型,其实平时我们用得更多的是 DateTime? 直接在类型T后面加一个问号,这两种是等效的。多亏了微软的语法糖。
我们来看看 System.Nullable<T> 到底是何物。

搜噶,原来是一个结构。还看到了我们属性的 HasValue和Value属性。原来竟这般简单。一个结构两个属性,一个存值,一个存是否有值。那么下面我们也来试试吧。

不好意思,让大家失望了。前面我们就说过了,值类型是不可以赋值null的(结构也是值类型)。
怎么办!怎么办!不对啊,微软自己也是定义的结构,它怎么可以直接赋值null呢。(奇怪,奇怪,毕竟是人家微软自己搞得,可能得到了特殊的待遇吧)
可是,这样就让我们止步了吗?NO!我们都知道,看微软的IL(中间语言)的时候,就像脱了它的衣服一样,很多时候不明白的地方都可以看个究竟,下面我们就去脱衣服。
首先,我们用几种不同的方式给可空类型赋值。
static void Main(string[] args)
{ System.Nullable<int> number1 = null; System.Nullable<int> number2 = new System.Nullable<int>(); System.Nullable<int> number3 = ; System.Nullable<int> number4 = new System.Nullable<int>(); Console.ReadKey();
}
然后用reflector看编译后的IL。

原来如此,可空类型的赋值直接等效于构造实例。赋null时其实就是调用空构造函数,有值时就就把值传入带参数的构造函数。(柳暗花明又一村。如此,我们是否可以接着上面截图中的 MyNullable<T> 继续模拟可空类型呢?且继续往下看。)
public struct MyNullable<T> where T : struct
{
//错误 1 结构不能包含显式的无参数构造函数
//还好 bool默认值就是false,所以这里不显示为 this._hasValue = false也不会有影响
//public MyNullable()
//{
// this._hasValue = false;
//}
public MyNullable(T value)//有参构造函数
{
this._hasValue = true;
this._value = value;
} private bool _hasValue; public bool HasValue//是否不为空
{
get { return _hasValue; }
} private T _value;
public T Value//值
{
get
{
if (!this._hasValue)//如没有值,还访问就抛出异常
{
throw new Exception(" 可为空的对象必须具有一个值");
}
return _value;
}
}
}
哟西,基本上已经模拟出了可空类型出来的。(但是我们还是不能直接赋值,只能通过构造函数的方式来使用自定义的可空类型)。
全部代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace 可空类型
{
public class Person
{
/// <summary>
/// 出生日期
/// </summary>
public DateTime BeginTime { get; set; }
/// <summary>
/// 死亡日期
/// </summary>
public MyNullable<DateTime> EndTiem { get; set; } //这里改用MyNullable
/// <summary>
/// 年龄
/// </summary>
public double Age
{
get
{
if (EndTiem.HasValue)//如果挂了(如果有值,证明死了)
{
return (EndTiem.Value - BeginTime).Days / ;
}
else//还没挂
{
return (DateTime.Now - BeginTime).Days / ;
}
}
}
} public struct MyNullable<T> where T : struct
{
//错误 1 结构不能包含显式的无参数构造函数
//还好 bool默认值就是false,所以这里不显示为 this._hasValue = false也不会有影响
//public MyNullable()
//{
// this._hasValue = false;
//}
public MyNullable(T value)//有参构造函数
{
this._hasValue = true;
this._value = value;
} private bool _hasValue; public bool HasValue//是否不为空
{
get { return _hasValue; }
} private T _value;
public T Value//值
{
get
{
if (!this._hasValue)//如没有值,还访问就抛出异常
{
throw new Exception(" 可为空的对象必须具有一个值");
}
return _value;
}
}
}
class Program
{
static void Main(string[] args)
{
Person p1 = new Person()
{
BeginTime = DateTime.Parse("1990-07-19")
}; Person p2 = new Person()
{
BeginTime = DateTime.Parse("1893-12-26"),
EndTiem = new MyNullable<DateTime>(DateTime.Parse("1976-09-09"))//这里使用MyNullable的有参构造函数
}; Console.WriteLine("我今年" + p1.Age + "岁。");
Console.WriteLine("毛爷爷活了" + p2.Age + "岁。"); Console.ReadKey();
} }
}
和系统的可空类型得出了相同的结果。

总结
- 可空类型是结构(也就是值类型)
- 所以可空类型的null值和引用类型的null是不一样的。(可空类型的并不是引用类型的null,而是用结构的另一种表示方式来表示null)

有同学问,怎么样才可以做到直接赋值呢?这个我也没有什么好的办法,或许需要编译器的支持。
以上内容都是胡说八道。希望能对您有那么一点点用处,感谢阅读。
============== 2016-06-05更新==============
上面我们提出了疑问“怎么样才可以做到直接赋值呢”,本来我是没有好的解决办法。这里要感谢我们的园友@冲杀给我提供了好的解决方案。
implicit(关键字用于声明隐式的用户定义类型转换运算符。)
public static implicit operator MyNullable<T>(T value)
{
return new MyNullable<T>(value);
}
只需要在 struct MyNullable<T> 中添加以上代码,就可以直接赋值了。(作用等效于是直接重写了“=”赋值符号)


完整代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace test
{
public class Person
{
/// <summary>
/// 出生日期
/// </summary>
public DateTime BeginTime { get; set; }
/// <summary>
/// 死亡日期
/// </summary>
public MyNullable<DateTime> EndTiem { get; set; } //这里改用MyNullable
/// <summary>
/// 年龄
/// </summary>
public double Age
{
get
{
if (EndTiem.HasValue)//如果挂了(如果有值,证明死了)
{
return (EndTiem.Value - BeginTime).Days / ;
}
else//还没挂
{
return (DateTime.Now - BeginTime).Days / ;
}
}
}
} public struct MyNullable<T> where T : struct
{
//错误 1 结构不能包含显式的无参数构造函数
//还好 bool默认值就是false,所以这里不显示为 this._hasValue = false也不会有影响
//public MyNullable()
//{
// this._hasValue = false;
//} public MyNullable(T value)//有参构造函数
{
this._hasValue = true;
this._value = value;
} private bool _hasValue; public bool HasValue//是否不为空
{
get { return _hasValue; }
} private T _value;
public T Value//值
{
get
{
if (!this._hasValue)//如没有值,还访问就抛出异常
{
throw new InvalidOperationException(" 可为空的对象必须具有一个值");
}
return _value;
}
} public static implicit operator MyNullable<T>(T value)
{
return new MyNullable<T>(value);
}
}
class Program
{
static void Main(string[] args)
{
Person p1 = new Person()
{
BeginTime = DateTime.Parse("1990-07-19")
}; Person p2 = new Person()
{
BeginTime = DateTime.Parse("1893-12-26"),
EndTiem = DateTime.Parse("1976-09-09")
//new MyNullable<DateTime>(DateTime.Parse("1976-09-09"))
//这里使用MyNullable的有参构造函数
}; Console.WriteLine("我今年" + p1.Age + "岁。");
Console.WriteLine("毛爷爷活了" + p2.Age + "岁。"); Console.ReadKey();
} }
}
如此,我们已经完成了自定义可空类型的直接赋值。但只是部分,如果想要赋值null呢?

同样还是出现了最开始的编译错误。我们想到既然上面的值赋值可以重新(隐式转换),那null应该也可以啊(null是引用类型的一个特定值)。
再加一个重载:
//隐式转换
public static implicit operator MyNullable<T>(string value)
{
if (value == null)
return new MyNullable<T>();
throw new Exception("赋值右边不能为字符串");
//这里不知道是否可以在编译期间抛出错误(或者怎样限制只能传null)
}
如此可以满足我们的需求了(并无异常)。

可惜美中不足,如果给 p2.EndTiem 赋值一个非空字符串时,要运行时才会报错(而系统的可空类型会在编译期就报错)。不知道大神们可有解!!
虽然如此,能做到直接赋值还是让我小小激动了一把。为此,特意查了下关键字 implicit operator ,又是让我小小激动了一把,我们不仅可以“重写”赋值,我们还可以“重写”+ - * / % & | ^ << >> == != > < >= <=等运算符。
下面我们先来“重写”下自定义可空类型的比较(==)运算符。
//"重写"比较运算符
public static bool operator ==(MyNullable<T> operand, MyNullable<T> operand2)
{
if (!operand.HasValue && !operand2.HasValue)
{
return true;
}
else if (operand.HasValue && operand2.HasValue)
{
if (operand2.Value.Equals(operand.Value))
{
return true;
}
}
return false;
} //"重写"比较运算符
public static bool operator !=(MyNullable<T> operand, MyNullable<T> operand2)
{
return !(operand == operand2);
}
Console.WriteLine("p1.EndTiem == null," + (p1.EndTiem == null).ToString());
Console.WriteLine("p2.EndTiem == null," + (p2.EndTiem == null).ToString());
Console.WriteLine("p1.EndTiem == DateTime.Parse(1976-09-09)," + (p1.EndTiem == DateTime.Parse("1976-09-09")).ToString());
Console.WriteLine("p2.EndTiem == DateTime.Parse(1976-09-09)," + (p2.EndTiem == DateTime.Parse("1976-09-09")).ToString());
p1.EndTiem = DateTime.Parse("2016-06-06");
p2.EndTiem = null;
Console.WriteLine();
Console.WriteLine("赋值 p1.EndTiem = DateTime.Parse(2016-06-06) p2.EndTiem = null 后:");
Console.WriteLine("p1.EndTiem == null," + (p1.EndTiem == null).ToString());
Console.WriteLine("p2.EndTiem == null," + (p2.EndTiem == null).ToString());
Console.WriteLine("p1.EndTiem == DateTime.Parse(2016-06-06)," + (p1.EndTiem == DateTime.Parse("2016-06-06")).ToString());
Console.WriteLine("p2.EndTiem == DateTime.Parse(2016-06-06)," + (p2.EndTiem == DateTime.Parse("2016-06-06")).ToString());

结果完全符合!
完整代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace test
{
public class Person
{
/// <summary>
/// 出生日期
/// </summary>
public DateTime BeginTime { get; set; }
/// <summary>
/// 死亡日期
/// </summary>
public MyNullable<DateTime> EndTiem { get; set; } //这里改用MyNullable
/// <summary>
/// 年龄
/// </summary>
public double Age
{
get
{
if (EndTiem.HasValue)//如果挂了(如果有值,证明死了)
{
return (EndTiem.Value - BeginTime).Days / ;
}
else//还没挂
{
return (DateTime.Now - BeginTime).Days / ;
}
}
}
} public struct MyNullable<T> where T : struct
{
//错误 1 结构不能包含显式的无参数构造函数
//还好 bool默认值就是false,所以这里不显示为 this._hasValue = false也不会有影响
//public MyNullable()
//{
// this._hasValue = false;
//} public MyNullable(T value)//有参构造函数
{
this._hasValue = true;
this._value = value;
} private bool _hasValue; public bool HasValue//是否不为空
{
get { return _hasValue; }
} private T _value;
public T Value//值
{
get
{
if (!this._hasValue)//如没有值,还访问就抛出异常
{
throw new InvalidOperationException(" 可为空的对象必须具有一个值");
}
return _value;
}
} //隐式转换
public static implicit operator MyNullable<T>(T value)
{
return new MyNullable<T>(value);
} //隐式转换
public static implicit operator MyNullable<T>(string value)
{
if (value == null)
return new MyNullable<T>();
throw new Exception("赋值右边不能为字符串");
//这里不知道是否可以在编译期间抛出错误(或者怎样限制只能传null)
} //"重写"比较运算符
public static bool operator ==(MyNullable<T> operand, MyNullable<T> operand2)
{
if (!operand.HasValue && !operand2.HasValue)
{
return true;
}
else if (operand.HasValue && operand2.HasValue)
{
if (operand2.Value.Equals(operand.Value))
{
return true;
}
}
return false;
} //"重写"比较运算符
public static bool operator !=(MyNullable<T> operand, MyNullable<T> operand2)
{
return !(operand == operand2);
}
}
class Program
{
static void Main(string[] args)
{
Person p1 = new Person()
{
BeginTime = DateTime.Parse("1990-07-19")
}; Person p2 = new Person()
{
BeginTime = DateTime.Parse("1893-12-26"),
EndTiem = DateTime.Parse("1976-09-09")
//new MyNullable<DateTime>(DateTime.Parse("1976-09-09"))
//这里使用MyNullable的有参构造函数
}; Console.WriteLine("我今年" + p1.Age + "岁。");
Console.WriteLine("毛爷爷活了" + p2.Age + "岁。");
Console.WriteLine(); Console.WriteLine("p1.EndTiem == null," + (p1.EndTiem == null).ToString());
Console.WriteLine("p2.EndTiem == null," + (p2.EndTiem == null).ToString());
Console.WriteLine("p1.EndTiem == DateTime.Parse(1976-09-09)," + (p1.EndTiem == DateTime.Parse("1976-09-09")).ToString());
Console.WriteLine("p2.EndTiem == DateTime.Parse(1976-09-09)," + (p2.EndTiem == DateTime.Parse("1976-09-09")).ToString()); p1.EndTiem = DateTime.Parse("2016-06-06");
p2.EndTiem = null;
Console.WriteLine();
Console.WriteLine("赋值 p1.EndTiem = DateTime.Parse(2016-06-06) p2.EndTiem = null 后:");
Console.WriteLine("p1.EndTiem == null," + (p1.EndTiem == null).ToString());
Console.WriteLine("p2.EndTiem == null," + (p2.EndTiem == null).ToString());
Console.WriteLine("p1.EndTiem == DateTime.Parse(2016-06-06)," + (p1.EndTiem == DateTime.Parse("2016-06-06")).ToString());
Console.WriteLine("p2.EndTiem == DateTime.Parse(2016-06-06)," + (p2.EndTiem == DateTime.Parse("2016-06-06")).ToString()); Console.ReadKey();
} }
}
转换关键字:operator、explicit与implicit解析资料:http://www.cnblogs.com/hunts/archive/2007/01/17/operator_explicit_implicit.html
大家还可以玩出更多的花样!!!
本文已同步至《C#基础知识巩固系列》
四、可空类型Nullable<T>到底是什么鬼的更多相关文章
- 【转】四、可空类型Nullable<T>到底是什么鬼
[转]四.可空类型Nullable<T>到底是什么鬼 值类型为什么不可以为空 首先我们都知道引用类型默认值都是null,而值类型的默认值都有非null. 为什么引用类型可以为空?因为引用类 ...
- 【C#】可空类型(Nullable)
C# 可空类型(Nullable) C# 提供了一个特殊的数据类型,nullable 类型(可空类型),可空类型可以表示其基础值类型正常范围内的值,再加上一个 null 值. 例如,Nullable& ...
- 可空类型 Nullable<T>
Nullable<T> 内部实现了显示和隐式转换 显示转换: public static explicit operator T(T? value) { return value.Valu ...
- 【C#】可空类型 NullAble<T>
在实际编写代码时候 , 会遇到很多场景, 需要将值置成空, 比如发货日期, 有可能是没有. 在没有可空类型之前, 程序都是用 魔值, 即为一个minValue或者常量, 来代表这个值为空, 也有用一 ...
- 可空类型Nullable
Nullable类型: 值类型变量默认为0,不可空,为了使它可空,出现了Nullable类型,类型前面加? 变为引用类型 值类型是没有null值的,比如int,DateTime,它们都有默认值.举个 ...
- 细说可空类型 nullable PropertyType
可空类型是System.Nullable结构体的实列.一个可空类型代表了相应值类型的正确范围附加null值.这么说来,其实也不是很明子,命题嘛,一般不求易懂,但求准确. 那我就来说说这可空类型吧,上次 ...
- 雷林鹏分享:C# 可空类型(Nullable)
C# 可空类型(Nullable) C# 可空类型(Nullable) C# 提供了一个特殊的数据类型,nullable 类型(可空类型),可空类型可以表示其基础值类型正常范围内的值,再加上一个 nu ...
- C#可空类型知多少
在项目中我们经常会遇到可为空类型,那么到底什么是可为空类型呢?下面我们将从4个方面为大家剖析. 1.可空类型基础知识 顾名思义,可空类型指的就是某个对象类型可以为空,同时也是System.Nullab ...
- C#2.0之可空类型
可空类型Nullable<T> 在C#2.0之前 ,值类型一直不可以为null,但是实际开发中常常会有这样的需求,比如结束时间. 为什么不可以为null 对于引用类型来说,null代表着空 ...
随机推荐
- 高大上的微服务可以很简单,使用node写微服务
安装 npm install m-service --save 使用 编写服务处理函数 // dir1/file1.js // 使用传入的console参数输出可以自动在日志里带上request id ...
- 前端学HTTP之重定向和负载均衡
前面的话 HTTP并不是独自运行在网上的.很多协议都会在HTTP报文的传输过程中对其数据进行管理.HTTP只关心旅程的端点(发送者和接收者),但在包含有镜像服务器.Web代理和缓存的网络世界中,HTT ...
- Android数据加密之SHA安全散列算法
前言: 对于SHA安全散列算法,以前没怎么使用过,仅仅是停留在听说过的阶段,今天在看图片缓存框架Glide源码时发现其缓存的Key采用的不是MD5加密算法,而是SHA-256加密算法,这才勾起了我的好 ...
- 学习ASP.NET Core, 怎能不了解请求处理管道[3]: 自定义一个服务器感受一下管道是如何监听、接收和响应请求的
我们在<服务器在管道中的"龙头"地位>中对ASP.NET Core默认提供的具有跨平台能力的KestrelServer进行了介绍,为了让读者朋友们对管道中的服务器具有更 ...
- 谈谈JS中的函数节流
好吧,一直在秋招中,都没怎么写博客了...今天赶紧来补一补才行...我发现,在面试中,讲到函数节流好像可以加分,尽管这并不是特别高深的技术,下面就聊聊吧! ^_^ 备注:以下内容部分来自<Jav ...
- css3圆形百分比进度条的实现原理
原文地址:css3圆形百分比进度条的实现原理 今天早上起来在查看jquery插件机制的时候,一不小心点进了css3圆形百分比进度条的相关文章,于是一发不可收拾,开始折腾了... 关于圆形圈的实现,想必 ...
- 微信公众号开发(一)--验证服务器地址的Java实现
现在主流上都用php写微信公众号后台,其实作为后端语言之一的java也可以实现. 这篇文章将对验证服务器地址这一步做出实现. 参考资料:1.慕课网-<初识java微信公众号开发>,2.微信 ...
- SQLite学习笔记(十)&&加密
随着移动互联网的发展,手机使用越来越广泛,sqlite作为手机端存储的一种解决方案,使用也非常普遍.但是sqlite本身安全特性却比较弱,比如不支持用户权限,只要能获取到数据库文件就能进行访问:另外也 ...
- 【QQ红包】手机发抢不到的口令红包
这方法95%的人都抢不了 在QQ输入框输入一个表情,例如:阴险那个表情 将表情剪切到口令红包的口令里 这时候口令里的那个表情表情变成了符号 将符号删去一格,然后全选.复制 然后返回到QQ输入框粘贴 然 ...
- osi(open system interconnection)模型的通俗理解
OSI模型的理解: 以你和你女朋友以书信的方式进行通信为例. 1.物理层:运输工具,比如火车.汽车. 2.数据链路层:相当于货物核对单,表明里面有些什么东西,接受的时候确认一下是否正确(CRC检验). ...