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有如下的基本規則和應用:
null為引用類型變量的默認值,為引用類型的概念范疇。
null不等同於0,"",string.Empty。
引用is或as模式對類型進行判斷或轉換時,需要做進一步的null判斷。
判斷一個變量是否為null,可以應用==或!=操作符來完成。
對任何值為nul的l變量操作,都會拋出NullReferenceException異常。
2 Nullable<T>(可空類型)
一直以來,null都是引用類型的特有產物,對值類型進行null操作將在編譯器拋出錯誤提示,例如:
//拋出編譯時錯誤
int i = null;
if (i == null)
{
Console.WriteLine("i is null.");
}
正如示例中所示,很多情況下作為開發人員,我們更希望能夠以統一的方式來處理,同時也希望能夠解決實際業務需求中對於“值”也可以為“空”這一實際情況的映射。因此,自.NET 2.0以來,這一特權被新的System.Nullable<T>(即,可空值類型)的誕生而打破,解除上述诟病可以很容易以下面的方式被實現:
//Nullable<T>解決了這一問題
int? i = null;
if (i == null)
{
Console.WriteLine("i is null.");
}
你可能很奇怪上述示例中並沒有任何Nullable的影子,實際上這是C#的一個語法糖,以下代碼在本質上是完全等效的:
int? i = null;
Nullable<int> i = null;
顯然,我們更中意以第一種簡潔而優雅的方式來實現我們的代碼,但是在本質上Nullable<T>和T?他們是一路貨色。
可空類型的偉大意義在於,通過Nullable<T>類型,.NET為值類型添加“可空性”,例如Nullable<Boolean>的值就包括了true、false和null,而Nullable<Int32>則表示值即可以為整形也可以為null。同時,可空類型實現了統一的方式來處理值類型和引用類型的“空”值問題,例如值類型也可以享有在運行時以NullReferenceException異常來處理。
另外,可空類型是內置於CLR的,所以它並非c#的獨門絕技,VB.NET中同樣存在相同的概念。
Nullable的本質(IL)
那麼我們如何來認識Nullable的本質呢?當你聲明一個:
Nullable<Int32> count = new Nullable<Int32>();
時,到底發生了什麼樣的過程呢?我們首先來了解一下Nullable在.NET中的定義:
public struct Nullable<T> 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<T>進行初始化時,將值類型賦給value,可以從其構造函數獲知:
public Nullable(T value)
{
this.value = value;
this.hasValue = true;
}
同時Nullable<T>實現相應的Equals、ToString、GetHashCode方法,以及顯式和隱式對原始值類型與可空類型的轉換。因此,在本質上Nullable可以看著是預定義的struct類型,創建一個Nullable<T>類型的IL表示可以非常清晰的提供例證,例如創建一個值為int型可空類型過程,其IL可以表示為:
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 2
.locals init (
[0] valuetype [mscorlib]System.Nullable`1<int32> a)
L_0000: nop
L_0001: ldloca.s a
L_0003: ldc.i4 0x3e8
L_0008: call instance void [mscorlib]System.Nullable`1<int32>::.ctor(!0)
L_000d: nop
L_000e: ret
}
對於可空類型,同樣需要必要的小結:
可空類型表示值為null的值類型。
不允許使用嵌套的可空類型,例如Nullable<Nullable<T>> 。
Nullable<T>和T?是等效的。
對可空類型執行GetType方法,將返回類型T,而不是Nullable<T>。
c#允許在可空類型上執行轉換和轉型,例如:
int? a = 100;
Int32 b = (Int32)a;
a = null;
同時為了更好的將可空類型於原有的類型系統進行兼容,CLR提供了對可空類型裝箱和拆箱的支持。
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模式的小結
有效解決對象為空的情況,為值為null提供可靠保證。
保證能夠返回有效的默認值,例如在一個IList<User> userList中,能夠保證任何情況下都有有效值返回,可以保證對userList操作的有效性,例如:
// Copyright : www.anytao.com
// Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0
public void SendMessageAll(List<User> userList)
{
//不需要對userList進行null判斷
foreach (User user in userList)
{
user.SendMessage();
}
}
提供統一判定的IsNull屬性。可以通過實現INullable接口,也可以通過Extension Method實現IsNull判定方法。
null object要保持原object的所有成員的不變性,所以我們常常將其實現為Sigleton模式。
Scott Doman說“在執行方法時返回null object而不是null值,可以避免NullReferenceExecption異常的發生”,這完全是對的。
5 結論
雖然形色匆匆,但是通過本文你可以基本了解關於null這個話題的方方面面,堆積到一起就是對一個概念清晰的把握和探討。技術的魅力,大概也正是如此而已吧,色彩斑斓的世界裡,即便是“什麼都沒有”的null,在我看來依然有很多很多。。。值得探索、思考和分享。
還有更多的null,例如LINQ中的null,SQL中的null,仍然可以進行探討,我們將這種思考繼續,所收獲的果實就越多。