C# 處理空值(Null)物件
如果真的沒有值,該怎麼辦?
系列文:
要處理的事情
我們撰寫程式時,有時候會遇到無法處理的情況,例如 LastLogin 這個參數,我們可以用 0 表示使用者今天有登入, 1表示他昨天有登入,但如果今天表達「使用者從未登入」我們也許會給 -1,但是這種我們稱之為魔術數字,其表達方式非常地不好,因為接手的人不一定會知道當初設計的人給 -1 的概念叫作未登入,所以必須要解決這種問題。
Null 的概念
Null可以是「不存在」、「沒有」、「無」或「空」的概念,雖然無數的文章或者研究指出 Null 本身造成的問題以及損失,甚至也有文章,例如這篇《補救null的策略》說明了 Null 的問題:
null的根本問題在於語意含糊不清,就字面來說,null可以是「不存在」、「沒有」、「無」或「空」的概念,在應用時,總是令人感到模稜兩可,也就讓開發者有了各自解釋的空間。當開發者想到「嘿!這邊可以沒有東西……」就直接放個null,或者是想到「嗯!沒什麼東西可以傳回……」,就不假思索地傳回個null,然後使用者就總是忘了檢查null,引發各種可能的錯誤。
不過這篇文章目的想要探討如何「合理地」使用 Null,所以關於可能引發的錯誤以及其解決方式可以參考《補救null的策略》。
Nullable<T> 介紹
Nullable<T> 用意是在讓本來沒有 null 特性的值型別(Value Type)能夠像參考型別(ReferenceType)一樣可以指派 null 或比較其是否為 null。例如待會範例程式碼中的 int 跟 DateTIme 原本都是 Value Type,所以要使用 Nullable<T> 讓它們可存 null。
透過指定 Null 解決魔術數字
在這邊,我們先定義好沒登入過的值定義為 Null。
我們建立一個名為 PlayerCharacter 的物件,然後定義 int? DaysSinceLastLogin ,其中 int? 是 Nullable<int> 的簡寫。
class PlayerCharacter
{
public string Name { get; set; }
public int? DaysSinceLastLogin { get; set; } // int? is shorthand of Nullable<int>
public DateTime? DateOfBirth { get; set; }
public bool? IsNoob { get; set; }
public PlayerCharacter()
{
DateOfBirth = null;
DaysSinceLastLogin = null;
}
}
當我們像 int? DaysSinceLastLogin 這樣賦予DaysSinceLastLogin可以存 null 之後,其結果可以看一下 PlayerCharacter 的建構式內的賦值,我們成功地讓 DaysSinceLastLogin 可以賦予 null 值。
接著,我們就可以透過判斷這個值是否為 Null,並且打印出不同的結果,我們把一開始的程式碼改寫成如下:
if (player.DaysSinceLastLogin == null)
Console.WriteLine("Player Since Last Login No specificed");
else
Console.WriteLine(player.DaysSinceLastLogin);
或者是使用 Nullable Class Property 跟 Method 來判斷物件是否為 Null
if (player.DaysSinceLastLogin.HasValue)
Console.WriteLine(player.DaysSinceLastLogin.Value);
else
Console.WriteLine("No Value for DaysSinceLastLogin");
判斷是否為 Null 後並且賦值
我們也可以使用 GetValueOrDefault() 來取得該物件的值,如果該屬性為 Null,我們回傳 -1 作為預設的回傳值。
int days = player.DaysSinceLastLogin.GetValueOrDefault(-1);
Console.WriteLine("{0} days since last login", days);
或者是使用三元運算子來回傳預設的賦值。
int days = player.DaysSinceLastLogin.HasValue ? player.DaysSinceLastLogin.Value : -1;
Null-coalescing Operator介紹
?? 這個運算子主要用在判別物件是否不為 null ,他的用途跟三元運算子功能一樣,且根據查到的一篇文章, ??有另外一個特性就是可以直接串連
我們可以將上面的程式碼改寫成如果 player.DaysSinceLastLogin 不是 Null,則我們回傳 DaysSinceLastLogin,否則就回傳 -1 給使用者。
int days = player.DaysSinceLastLogin.HasValue ?
player.DaysSinceLastLogin.Value : -1;
//equal to
int days = player.DaysSinceLastLogin ?? -1;
Null-conditional Operator
如果今天一個物件為空值
PlayerCharacter pc = null;
我們可以利用傳統方式:
if (pc != null)
days = pc.DaysSinceLastLogin?? -1;
else
days = -1;
也可以用 C# 6.0 的特性
int days = pc?.DaysSinceLastLogin ?? -1;
其中 pc 右邊的 ? 是先確認 pc 物件是否存在,若存在才會去檢 DaysSinceLogin property 是否存在,如果想要知道他的演進過程,可以參閱這篇文章
檢查 Array 內物件的空值
我們可以透過前面的做法如法炮製
PlayerCharacter[] players = new PlayerCharacter[3]
{
new PlayerCharacter {Name = "Kevin"},
new PlayerCharacter(), // Name = null
null // PlayerCharacter = null
};
string p1 = players?[0]?.Name;
string p2 = players?[1]?.Name;
string p3 = players?[2]?.Name;
我們透過使用 ?[ 來檢查該 array index 內的物件是為空值,後面用 ?. 來確保players[0] 不為 null,且 players[0].Name 也不為 null 時,會回傳值,否則都回傳 null。
結語
Null 可以是一個解決方案,這個作法想要解決的問題有所謂的 Magic Number 。但是建議還是盡量避免使用 Null,在過去使用 Null 的情況中,工程師定義 Method 中傳回 Null,通常代表著 Client 必須檢查該值是否為 Null,來決定是否使用預設值。所以在程式設計中,還是要考量情境,來掉用適合的解法。