值类型为什么不可以为空

首先我们都知道引用类型默认值都是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)

有同学问,怎么样才可以做到直接赋值呢?这个我也没有什么好的办法,或许需要编译器的支持。

以上内容都是胡说八道。希望能对您有那么一点点用处,感谢阅读。

(首发链接:http://www.cnblogs.com/zhaopei/p/5537759.html )


============== 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>到底是什么鬼的更多相关文章

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

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

  2. 【C#】可空类型(Nullable)

    C# 可空类型(Nullable) C# 提供了一个特殊的数据类型,nullable 类型(可空类型),可空类型可以表示其基础值类型正常范围内的值,再加上一个 null 值. 例如,Nullable& ...

  3. 可空类型 Nullable<T>

    Nullable<T> 内部实现了显示和隐式转换 显示转换: public static explicit operator T(T? value) { return value.Valu ...

  4. 【C#】可空类型 NullAble<T>

    在实际编写代码时候 ,  会遇到很多场景, 需要将值置成空, 比如发货日期, 有可能是没有. 在没有可空类型之前, 程序都是用 魔值, 即为一个minValue或者常量, 来代表这个值为空, 也有用一 ...

  5. 可空类型Nullable

    Nullable类型: 值类型变量默认为0,不可空,为了使它可空,出现了Nullable类型,类型前面加?  变为引用类型 值类型是没有null值的,比如int,DateTime,它们都有默认值.举个 ...

  6. 细说可空类型 nullable PropertyType

    可空类型是System.Nullable结构体的实列.一个可空类型代表了相应值类型的正确范围附加null值.这么说来,其实也不是很明子,命题嘛,一般不求易懂,但求准确. 那我就来说说这可空类型吧,上次 ...

  7. 雷林鹏分享:C# 可空类型(Nullable)

    C# 可空类型(Nullable) C# 可空类型(Nullable) C# 提供了一个特殊的数据类型,nullable 类型(可空类型),可空类型可以表示其基础值类型正常范围内的值,再加上一个 nu ...

  8. C#可空类型知多少

    在项目中我们经常会遇到可为空类型,那么到底什么是可为空类型呢?下面我们将从4个方面为大家剖析. 1.可空类型基础知识 顾名思义,可空类型指的就是某个对象类型可以为空,同时也是System.Nullab ...

  9. C#2.0之可空类型

    可空类型Nullable<T> 在C#2.0之前 ,值类型一直不可以为null,但是实际开发中常常会有这样的需求,比如结束时间. 为什么不可以为null 对于引用类型来说,null代表着空 ...

随机推荐

  1. CMS模板应用调研问卷

    截止目前,已经有数十家网站与我们合作,进行了MIP化改造,在搜索结果页也能看到"闪电标"的出现.除了改造方面的问题,MIP项目组被问到最多的就是:我用了wordpress,我用了织 ...

  2. ASP.NET Core 1.1.0 Release Notes

    ASP.NET Core 1.1.0 Release Notes We are pleased to announce the release of ASP.NET Core 1.1.0! Antif ...

  3. Web Api 入门实战 (快速入门+工具使用+不依赖IIS)

    平台之大势何人能挡? 带着你的Net飞奔吧!:http://www.cnblogs.com/dunitian/p/4822808.html 屁话我也就不多说了,什么简介的也省了,直接简单概括+demo ...

  4. spring源码分析之@ImportSelector、@Import、ImportResource工作原理分析

    1. @importSelector定义: /** * Interface to be implemented by types that determine which @{@link Config ...

  5. Redis百亿级Key存储方案(转)

    1 需求背景 该应用场景为DMP缓存存储需求,DMP需要管理非常多的第三方id数据,其中包括各媒体cookie与自身cookie(以下统称supperid)的mapping关系,还包括了supperi ...

  6. 前端制作动画的几种方式(css3,js)

    制作动态的网页是是前端工程师必备的技能,很好的实现动画能够极大的提高用户体验,增强交互效果,那么动画有多少实现方式,一直对此有选择恐惧症的我就总结一下,以便在开发的时候选择最好的实现方式. 1.css ...

  7. UVA, 10336 Rank the Languages

    难点在于:递归函数和输出: #include <iostream> #include <vector> #include <algorithm> #include ...

  8. 免费公开课,讲解强大的文档集成组件Aspose,现在可报名

    课程①:Aspose.Total公开课内容:讲解全能型文档管理工具Aspose.Total主要功能及应用领域时间:2016-11-24 14:30 (暂定)报名地址:http://training.e ...

  9. .JavaWeb文件上传和FileUpload组件使用

    .JavaWeb文件上传 1.自定义上传 文件上传时的表单设计要符合文件提交的方式: 1.提交方式:post 2.表单中有文件上传的表单项:<input type="file" ...

  10. 开始webservice了

    一.WebService到底是什么 一言以蔽之:WebService是一种跨编程语言和跨操作系统平台的远程调用技术. 所谓跨编程语言和跨操作平台,就是说服务端程序采用java编写,客户端程序则可以采用 ...