说在,开篇之前 |
null、nullable、??运算符、null object模式,这些闪亮的概念在你眼前晃动,我们有理由相信“存在即合理”,事实上,null不光合理,而且重要。本文,从null的基本认知开始,逐层了解可空类型、??运算符和null object模式,在循序之旅中了解不一样的null。 你必须知道的.NET,继续全新体验,分享更多色彩。 www.anytao.com |
1 从什么是null开始?
null,一个值得尊敬的数据标识。
一般说来,null表示空类型,也就是表示什么都没有,但是“什么都没有”并不意味“什么都不是”。实际上,null是如此的重要,以致于在JavaScript中,Null类型就作为5种基本的原始类型之一,与Undefined、Boolean、Number和String并驾齐驱。这种重要性同样表现在.NET中,但是一定要澄清的是,null并不等同于0,"",string.Empty这些通常意义上的“零”值概念。相反,null具有实实在在的意义,这个意义就是用于标识变量引用的一种状态,这种状态表示没有引用任何对象实例,也就是表示“什么都没有”,既不是Object实例,也不是User实例,而是一个空引用而已。
在上述让我都拗口抓狂的表述中,其实中心思想就是澄清一个关于null意义的无力诉说,而在.NET中null又有什么实际的意义呢?
在.NET中,null表示一个对象引用是无效的。作为引用类型变量的默认值,null是针对指针(引用)而言的,它是引用类型变量的专属概念,表示一个引用类型变量声明但未初始化的状态,例如:
object obj = null;
此时obj仅仅是一个保存在线程栈上的引用指针,不代表任何意义,obj未指向任何有效实例,而被默认初始化为null。
object obj和object obj = null的区别?
那么,object obj和object obj = null有实际的区别吗?答案是:有。主要体现在编译器的检查上。默认情况下,创建一个引用类型变量时,CLR即将其初始化为null,表示不指向任何有效实例,所以本质上二者表示了相同的意义,但是有有所区别:
// Copyright : www.anytao.com
// Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0
//编译器检测错误:使用未赋值变量obj
//object obj;
//编译器理解为执行了初始化操作,所以不引发编译时错误
object obj = null;
if (obj == null)
{
//运行时抛出NullReferenceException异常
Console.WriteLine(obj.ToString());
}
注:当我把这个问题抛给几个朋友时,对此的想法都未形成统一的共识,几位同志各有各的理解,也各有个的道理。当然,我也慎重的对此进行了一番探讨和分析,但是并未形成完全100%确定性的答案。不过,在理解上我更倾向于自己的分析和判断,所以在给出上述结论的基础上,也将这个小小的思考留给大家来探讨,好的思考和分析别忘了留给大家。事实上,将
static void Main(string[] args)
{
object o;
object obj = null;
}
反编译为IL时,二者在IL层还是存在一定的差别:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] object o,
[1] object obj)
L_0000: nop
L_0001: ldnull
L_0002: stloc.1
L_0003: ret
}
前者没有发生任何附加操作;而后者通过ldnull指令推进一个空引用给evaluation stack,而stloc则将空引用保存。
回到规则
在.NET中,对null有如下的基本规则和应用:
![]() |
www.anytao.com |
2 Nullable
一直以来,null都是引用类型的特有产物,对值类型进行null操作将在编译器抛出错误提示,例如:
//抛出编译时错误
int i = null;
if (i == null)
{
Console.WriteLine("i is null.");
}
正如示例中所示,很多情况下作为开发人员,我们更希望能够以统一的方式来处理,同时也希望能够解决实际业务需求中对于“值”也可以为“空”这一实际情况的映射。因此,自.NET 2.0以来,这一特权被新的System.Nullable
//Nullable解决了这一问题
int? i = null;
if (i == null)
{
Console.WriteLine("i is null.");
}
你可能很奇怪上述示例中并没有任何Nullable的影子,实际上这是C#的一个语法糖,以下代码在本质上是完全等效的:
int? i = null;
Nullable i = null;
显然,我们更中意以第一种简洁而优雅的方式来实现我们的代码,但是在本质上Nullable
可空类型的伟大意义在于,通过Nullable
另外,可空类型是内置于CLR的,所以它并非c#的独门绝技,VB.NET中同样存在相同的概念。
Nullable的本质(IL)
那么我们如何来认识Nullable的本质呢?当你声明一个:
Nullable count = new Nullable();
时,到底发生了什么样的过程呢?我们首先来了解一下Nullable在.NET中的定义:
public struct Nullable where T : struct
{
private bool hasValue;
internal T value;
public Nullable(T value);
public bool HasValue { get; }
public T Value { get; }
public T GetValueOrDefault();
public T GetValueOrDefault(T defaultValue);
public override bool Equals(object other);
public override int GetHashCode();
public override string ToString();
public static implicit operator T?(T value);
public static explicit operator T(T? value);
}
根据上述定义可知,Nullable本质上仍是一个struct为值类型,其实例对象仍然分配在线程栈上。其中的value属性封装了具体的值类型,Nullable
public Nullable(T value)
{
this.value = value;
this.hasValue = true;
}
同时Nullable
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 2
.locals init (
[0] valuetype [mscorlib]System.Nullable`1 a)
L_0000: nop
L_0001: ldloca.s a
L_0003: ldc.i4 0x3e8
L_0008: call instance void [mscorlib]System.Nullable`1::.ctor(!0)
L_000d: nop
L_000e: ret
}
对于可空类型,同样需要必要的小结:
int? a = 100;
Int32 b = (Int32)a;
a = null;
3 ??运算符
在实际的程序开发中,为了有效避免发生异常情况,进行null判定是经常发生的事情,例如对于任意对象执行ToString()操作,都应该进行必要的null检查,以免发生不必要的异常提示,我们常常是这样实现的:
object obj = new object();
string objName = string.Empty;
if (obj != null)
{
objName = obj.ToString();
}
Console.WriteLine(objName);
然而这种实现实在是令人作呕,满篇的if语句总是让人看着浑身不适,那么还有更好的实现方式吗,我们可以尝试(? :)三元运算符:
object obj = new object();
string objName = obj == null ? string.Empty : obj.ToString();
Console.WriteLine(objName);
上述obj可以代表任意的自定义类型对象,你可以通过覆写ToString方法来输出你想要输出的结果,因为上述实现是如此的频繁,所以.NET 3.0中提供了新的操作运算符来简化null值的判断过程,这就是:??运算符。上述过程可以以更加震撼的代码表现为:
// Copyright : www.anytao.com
// Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0
object obj = null;
string objName = (obj ?? string.Empty).ToString();
Console.WriteLine(objName);
那么??运算符的具体作用是什么呢?
??运算符,又称为null-coalescing operator,如果左侧操作数为null,则返回右侧操作数的值, 如果不为null则返回左侧操作数的值。它既可以应用于可空类型,有可以应用于引用类型。
![]() |
4 Nulll Object模式
模式之于设计,正如秘笈之于功夫。正如我们前文所述,null在程序设计中具有举足轻重的作用,因此如何更优雅的处理“对象为空”这一普遍问题,大师们提出了Null Object Pattern概念,也就是我们常说的Null Object模式。例如Bob大叔在《敏捷软件开发--原则、模式、实践》一书,Martin Fowler在《Refactoring: Improving the Design of Existing Code》一书,都曾就Null Object模式展开详细的讨论,可见23中模式之外还是有很多设计精髓,可能称为模式有碍经典。但是仍然值得我们挖据、探索和发现。
下面就趁热打铁,在null认识的基础上,对null object模式进行一点探讨,研究null object解决的问题,并提出通用的null object应用方式。
解决什么问题?
简单来说,null object模式就是为对象提供一个指定的类型,来代替对象为空的情况。说白了就是解决对象为空的情况,提供对象“什么也不做”的行为,这种方式看似无聊,但却是很聪明的解决之道。举例来说,一个User类型对象user需要在系统中进行操作,那么典型的操作方式是:
if (user != null)
{
manager.SendMessage(user);
}
这种类似的操作,会遍布于你的系统代码,无数的if判断让优雅远离了你的代码,如果大意忘记null判断,那么只有无情的异常伺候了。于是,Null object模式就应运而生了,对User类实现相同功能的NullUser类型,就可以有效的避免繁琐的if和不必要的失误:
// Copyright : www.anytao.com
// Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0
public class NullUser : IUser
{
public void Login()
{
//不做任何处理
}
public void GetInfo() { }
public bool IsNull
{
get { return true; }
}
}
IsNull属性用于提供统一判定null方式,如果对象为NullUser实例,那么IsNull一定是true的。
那么,二者的差别体现在哪儿呢?其实主要的思路就是将null value转换为null object,把对user == null这样的判断,转换为user.IsNull虽然只有一字之差,但是本质上是完全两回事儿。通过null object模式,可以确保返回有效的对象,而不是没有任何意义的null值。同时,“在执行方法时返回null object而不是null值,可以避免NullReferenceExecption异常的发生。”,这是来自Scott Dorman的声音。
通用的null object方案
下面,我们实现一种较为通用的null object模式方案,并将其实现为具有.NET特色的null object,所以我们采取实现.NET中INullable接口的方式来实现,INullable接口是一个包括了IsNull属性的接口,其定义为:
public interface INullable
{
// Properties
bool IsNull { get; }
}
仍然以User类为例,实现的方案可以表达为:
图中仅仅列举了简单的几个方法或属性,旨在达到说明思路的目的,其中User的定义为:
// Copyright : www.anytao.com
// Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0
public class User : IUser
{
public void Login()
{
Console.WriteLine("User Login now.");
}
public void GetInfo()
{
Console.WriteLine("User Logout now.");
}
public bool IsNull
{
get { return false; }
}
}
而对应的NullUser,其定义为:
// Copyright : www.anytao.com
// Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0
public class NullUser : IUser
{
public void Login()
{
//不做任何处理
}
public void GetInfo() { }
public bool IsNull
{
get { return true; }
}
}
同时通过UserManager类来完成对User的操作和管理,你很容易思考通过关联方式,将IUser作为UserManger的属性来实现,基于对null object的引入,实现的方式可以为:
// Copyright : www.anytao.com
// Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0
class UserManager
{
private IUser user = new User();
public IUser User
{
get { return user; }
set
{
user = value ?? new NullUser();
}
}
}
当然有效的测试是必要的:
public static void Main()
{
UserManager manager = new UserManager();
//强制为null
manager.User = null;
//执行正常
manager.User.Login();
if (manager.User.IsNull)
{
Console.WriteLine("用户不存在,请检查。");
}
}
通过强制将User属性实现为null,在调用Login时仍然能够保证系统的稳定性,有效避免对null的判定操作,这至少可以让我们的系统少了很多不必要的判定代码。
详细的代码可以通过本文最后的下载空间进行下载。实际上,可以通过引入Facotry Method模式来构建对于User和NullUser的创建工作,这样就可以完全消除应用if进行判断的僵化,不过那是另外一项工作罢了。
当然,这只是null object的一种实现方案,在此对《Refactoring》一书的示例进行改良,完成更具有.NET特色的null object实现,你也可以请NullUser继承Use并添加相应的IsNull判定属性来完成。
借力c# 3.0的Null object
在C# 3.0中,Extension Method(扩展方法)对于成就LINQ居功至伟,但是Extension Method的神奇远不是止于LINQ。在实际的设计中,灵活而巧妙的应用,同样可以给你的设计带来意想不到的震撼,以上述User为例我们应用Extension Method来取巧实现更简洁IsNull判定,代替实现INullable接口的方法而采用更简单的实现方式。重新构造一个实现相同功能的扩展方法,例如:
// Copyright : www.anytao.com
// Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0
public static class UserExtension
{
public static bool IsNull(this User user)
{
return null == user;
}
}
null object模式的小结
// Copyright : www.anytao.com
// Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0
public void SendMessageAll(List userList)
{
//不需要对userList进行null判断
foreach (User user in userList)
{
user.SendMessage();
}
}
5 结论
虽然形色匆匆,但是通过本文你可以基本了解关于null这个话题的方方面面,堆积到一起就是对一个概念清晰的把握和探讨。技术的魅力,大概也正是如此而已吧,色彩斑斓的世界里,即便是“什么都没有”的null,在我看来依然有很多很多。。。值得探索、思考和分享。
还有更多的null,例如LINQ中的null,SQL中的null,仍然可以进行探讨,我们将这种思考继续,所收获的果实就越多。
Anytao | 2008-07-31 | 你必须知道的.NET
http://www.anytao.com/ | Blog: http://anytao.cnblogs.com/ | Anytao原创作品,转贴请注明作者和出处,留此信息。
参考文献
(Book)Martin Fowler,Refactoring: Improving the Design of Existing Code
(cnblogs)zhuweisky,使用Null Object设计模式
(blogs)Scott Dorman,Null Object pattern
温故知新
[开篇有益]
[第一回:恩怨情仇:is和as]
[第二回:对抽象编程:接口和抽象类]
[第三回:历史纠葛:特性和属性]
[第四回:后来居上:class和struct]
[第五回:深入浅出关键字---把new说透]
[第六回:深入浅出关键字---base和this]
[第七回:品味类型---从通用类型系统开始]
[第八回:品味类型---值类型与引用类型(上)-内存有理]
[第九回:品味类型---值类型与引用类型(中)-规则无边]
[第十回:品味类型---值类型与引用类型(下)-应用征途]
[第十一回:参数之惑---传递的艺术(上)]
[第十二回:参数之惑---传递的艺术(下)]
[第十三回:从Hello, world开始认识IL]
[第十四回:认识IL代码---从开始到现在]
[第十五回:继承本质论]
[第十六回:深入浅出关键字---using全接触]
[第十七回:貌合神离:覆写和重载]
[第十八回:对象创建始末(上)]
[第十九回:对象创建始末(下)]
[第二十回:学习方法论]
#7楼 2008-07-31 08:38 荒芜
对于NullObject Pattern应该不会大量用到把.
我怎么感觉给类实现个INullable,有点不需要!
或者说我们判断....== null,未尝不可!
呵呵!
支持(0) 反对(0)
#11楼 2008-07-31 08:45 菜菜灰
?? .NET 2.0也存在的吧,不是在.net 3.0才有的吧
支持(0) 反对(0)
#12楼 2008-07-31 08:48 横刀天笑
@荒芜
对于一个对自己的代码追求完美的程序员来说null object pattern非常漂亮。如果不使用null object pattern,那么代码中到处会充斥着这样的代码:
if(a != null)
{
a.DoSomething();
}
注意,是到处,并不是一个地方出现。
还有别的地方的用处:
第一个:测试的时候,测试驱动开发,以测试先行。比如一个Blog对象,你要对Blog进行测试先行,那么你可以做一个IBlog接口,然后先实现一个“假的”InvalidBlog对象,进行测试。慢慢的添加功能,最后迭代到你要实现的Blog。
第二个:比如做用户验证的时候吧,你可以做一个IUser接口,一个InvalidUser,一个ValidUser,当验证失败返回InvalidUser,验证成功返回ValidUser。而调用验证这一层的客户代码只需要面对IUser这个接口就可以了。
上面的可能和nullobject pattern的初衷有点背离,不过也是nullobject pattern的演绎。看你怎么用了,我觉得nullobject pattern简单而优美
支持(0) 反对(0)
#30楼 2008-07-31 10:05 helloworld22
public void SendMessageAll(List
{
//不需要对userList进行null判断
foreach (User user in userList)
{
user.SendMessage();
}
}
但是user.SendMessage()之前,set user = null还是会报错吧。
public void SendMessageAll(List
{
//不需要对userList进行null判断
foreach (User user in userList)
{
user = null;
user.SendMessage();
}
}
支持(0) 反对(0)
#32楼 2008-07-31 10:08 primeli
问下啊,String s = null 与 String s = String.Empty 和 String s = "" 这3个有什么不同?
支持(0) 反对(0)
#33楼 2008-07-31 10:19 阿德斯基
--引用--------------------------------------------------
菜菜灰: ?? .NET 2.0也存在的吧,不是在.net 3.0才有的吧
--------------------------------------------------------
我测试了下 果然在.net2.0下成立 本人不才 头一回知道.net2.0下可以这样用 一直用一个耳朵的,今天长见识了
支持(0) 反对(0)
#34楼 2008-07-31 10:28 妖居
全面!精辟!
建议楼主再讲讲DBNull和null的关系。我为了做CRM系统特意写了个DBNullable
另外借地方说个VB中关于null的问题。IIf函数允许VB像C#一样使用三元运算符 ? : 第一个参数是一个返回Boolean的判断,然后两个参数分别是为True和False的时候的返回值。但是和C#不同他的后两个参数是优先运算的。所以如果用IIf(user Is Nothing, String.Empty, user.ToString())则会出现运行时异常。
支持(0) 反对(0)
#35楼 2008-07-31 10:29 Angel Lucifer
在我看来,Nulll Object模式完全属于设计过度,也没看出有多优雅。
如果你的项目使用类似的思想,那可要质疑你的程序性能了。
支持(0) 反对(0)
#36楼 2008-07-31 10:37 Angel Lucifer
@非主流程序员
null 的含义,楼主已经讲的很清楚了。
剩下的
String.Empty == "",呵呵。
支持(0) 反对(0)
#38楼 2008-07-31 11:15 Duron800
--引用--------------------------------------------------
Angel Lucifer: @非主流程序员
null 的含义,楼主已经讲的很清楚了。
剩下的
String.Empty == "",呵呵。
--------------------------------------------------------
string.Empty好像不会提示"never used"
支持(0) 反对(0)
#39楼 2008-07-31 11:16 Klesh Wong
这里的Null Object与直接判断null优雅在哪里?
不过是把 object == null 这个判断包装在另一个方法而已,除了罗索和多一层调用看不出有什么优势啊。
Null Object不是这么用的吧?不如举一下Logging的例子更好,
Null Logging看起来就合理多了。
支持(0) 反对(0)
#40楼 2008-07-31 11:38 横刀天笑
@Angel Lucifer
@Klesh Wong
嗯,我也觉得LZ这里的NullObject Pattern的例子没有选好,这样写实在是有点设计过度,不过NullObject Pattern确实很有用的,null object不仅仅是一个"null"对象,还可以理解为无效对象或者非法对象,也就是表示一种对象状态,所以程序里有这样的情况可以使用NullObject Pattern。
支持(0) 反对(0)
#41楼 2008-07-31 11:52 怪怪
晕倒, 原来你还在在意那天说的人家传过来的玩意是不是null的问题。
NullObject不见得一定就是设计过度, 不过也不见得处处使用。 比如我自己, if(null == xx || xx.length == 0)这种代码已经打顺手了 -___-
如果嫌它丑, 可以写一个静态的Guard类,然后Guard.IsNull(obj),Guard.IsNullOrEmpty(obj)这样子。 主要是这些写法是一个明确的表达。
而NullObject尤其是你最后的那种Extension,看起来代码虽然“优雅”,但是在一些情况下, 会把必要的信息隐藏了, 从而存在不易察觉的隐患。
支持(0) 反对(0)
#42楼 2008-07-31 12:00 Klesh Wong
@横刀天笑
是这样子的,Null Object当然是相当滴有用,就比如说Logging,健壮的程序很多地方都是需要Logging的,但在某些时候可能又不需要进行logging,在每一次地方使用null判断是非常罗索的事情,所以一个NullLogger会很有用,NullLogger实行ILogger的行为但其实不做任何logging,这就是NullObject的美妙之处——使程序变得简洁明了。
但LZ的例子则恰好相反,不但没有使程序更加简洁,反而变得罗索。。。
支持(0) 反对(0)
#43楼 2008-07-31 12:03 菜菜灰
回楼上,2.0本身就提供这样一个方法
String.IsNullOrEmpty(string value)
支持(0) 反对(0)
#44楼 2008-07-31 12:08 怪怪
@菜菜灰
问题是你要防止NullOrEmpty的类型, 不只string一种~。
支持(0) 反对(0)
1
#46楼 2008-07-31 13:07 Klesh Wong
@怪怪
那根本就不优雅,原则上来说user.IsNull根本就违反直觉了吧。
如果user不存在则返回null或者直接抛出异常我想都是大家可以接受的行为。
相反,你返回了一个非null的实例,是不是就是暗示了user就是存在的,最后还要调用它的"IsNull"来判定它本身是不是null,这个逻辑上好像挺绕。就好像在问一个不存在的人“你存不存在”这样子。
静态类的判断也是很好的,没有逻辑上的问题。就好像你问一个第三者这个人存不存在。直接判断null就相当于自己看看那个人存不存在。
支持(0) 反对(0)
#47楼 [楼主] 2008-07-31 13:55 Anytao
@荒芜
INullable只是在设计层面,提供一个统一的契约,由实现null object的类来遵守;而关于以obj == null方式进行判断,很多时候还是无可避免,因为你不可能给系统中所有的类实现相应的NullObject对应类,同时很多临时变量还是无可避免的回到obj == null
引用刘晓飞:User.IsNull违反了OOD
支持(0) 反对(0)