程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> [你必須知道的.NET]第二十一回:認識全面的null

[你必須知道的.NET]第二十一回:認識全面的null

編輯:關於.NET

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,仍然可以進行探討,我們將這種思考繼續,所收獲的果實就越多。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved