前言
本文來聊一聊我們經常會做的空值檢查問題,從一個簡單的空值檢查Any Where,到設計模式的NullObjectPattern,再到C#6.0“可能”會提供的文法,讓我們體驗一次語言開發上的“持續改進”,Let’s go~
什麼是空引用異常
作為一個敲過代碼的碼農來說,似乎沒有誰沒有遇到過NullReferenceException這個問題,有些時候當方法内部調用一個屬性、方法(委托)時,我們控制這些屬性在“外部”的表現(當然某些情況下使用ref關鍵字除外),是以我們要在方法的内部去判斷屬性、委托方法是否為Null來避免可能的、錯誤使用上帶來的空引用異常,這樣當我們知道如果對象為Null的話,我們會實作符合我們“預期”的行為。
解決空引用異常---Check Any Where
這很簡單,我隻要在需要用的地方檢查一下是否為Null就可以了。是的,這非常簡單,語義也很清晰,但是當你要重複檢查一個對象實體10000萬次時,你的代碼中将存在10000個如下代碼段:
public void Check()
{
if (Person.AlivePerson() != null)
{
Person.AlivePerson().KeepAlive = true;
}
}
你能容忍這樣的行為嗎?
If(OK)
Continue;
Else
Close;
應用NullObject設計模式
NullObjectPattern出自forth by Gamma(設計模式4人組),核心内容是:提供一個給定對象的空值代理,空值代理中提供不做任何事情的方法實作。
接下來讓我們看看維基百科上的C#實作:
// compile as Console Application, requires C# 3.0 or higher
using System;
using System.Linq;
namespace MyExtensionWithExample {
public static class StringExtensions {
public static int SafeGetLength(this string valueOrNull) {
return (valueOrNull ?? string.Empty).Length;
}
}
public static class Program {
// define some strings
static readonly string[] strings = new [] { "Mr X.", "Katrien Duck", null, "Q" };
// write the total length of all the strings in the array
public static void Main(string[] args) {
var query = from text in strings select text.SafeGetLength(); // no need to do any checks here
Console.WriteLine(query.Sum());
// The output will be: 18
}
}
}
在C#語言中,我們通過靜态的擴充方法來實作将檢查方式統一在方法内部,而不是寫的到處都是,上面的例子中是在String類上實作了一個SafeGetLength擴充方法,将為所有String類型提供了一個方法,這樣我們在“代碼整潔”上又進了一步。
下面我們再來看一個更常用的例子---來自于StackOverFlow
public static class EventExtensions
{
public static void Raise<T>(this EventHandler<T> evnt, object sender, T args)
where T : EventArgs
{
if (evnt != null)
{
evnt(sender, args);
}
}
}
最後,說一個細節問題,以上代碼均沒有實作“線程安全”,在大牛Eric Lippert的文章中針對線程安全有過一個更精彩的讨論,請戳這裡。
改進後的代碼時在方法内部增加了一個臨時變量,作為方法内部的拷貝,實作線程安全,如果有疑問請參考我的《C#堆vs棧》中對方法内部變量在堆棧上的表現一章。
public class SomeClass
{
public event EventHandler<EventArgs> MyEvent;
private void DoSomething()
{
var tmp = MyEvent;
tmp.Raise(this, EventArgs.Empty);
}
}
更“潮”的方式-C#6.0文法
來自MSDN Magazine的Mark Michaelis(《C#本質論》作者)給我們介紹了C#6.0在語言可能帶來的新改進,其中就有針對“Null條件運算符”的改進。
C#6.0更多參考:
Part One: https://msdn.microsoft.com/zh-cn/magazine/dn683793.aspx
Part Two: https://msdn.microsoft.com/zh-cn/magazine/dn802602.aspx
即使是 .NET 開發新手,也可能非常熟悉 NullReferenceException。有一個例外是幾乎總是會指出一個 Bug,因為開發人員在調用 (null) 對象的成員之前未進行充分的 null 檢查。請看看以下示例:
public static string Truncate(string value, int length)
{
string result = value;
if (value != null) // Skip empty string check for elucidation
{
result = value.Substring(0, Math.Min(value.Length, length));
}
return result;
}
如果不進行 null 檢查,此方法會引發 NullReferenceException。盡管這很簡單,但檢查字元串參數是否為 null 的過程卻稍微有些繁瑣。通常,考慮到比較的頻率,該繁瑣的方法可能沒有必要。C# 6.0 包括一個新的 null 條件運算符,可幫助您更加簡便地編寫這些檢查:
public static string Truncate(string value, int length)
{
return value?.Substring(0, Math.Min(value.Length, length));
}
[TestMethod]
public void Truncate_WithNull_ReturnsNull()
{
Assert.AreEqual<string>(null, Truncate(null, 42));
}
根據 Truncate_WithNull_ReturnsNull 方法所示範的内容,如果對象的值實際上為 null,則 null 條件運算符将傳回 null。這帶來了一個問題,即 null 條件運算符在調用鍊中出現時會是什麼情況?如以下示例中所示:
public static string AdjustWidth(string value, int length)
{
return value?.Substring(0, Math.Min(value.Length, length)).PadRight(length);
}
[TestMethod]
public void AdjustWidth_GivenInigoMontoya42_ReturnsInigoMontoyaExtended()
{
Assert.AreEqual<int>(42, AdjustWidth("Inigo Montoya", 42).Length);
}
盡管 Substring 是通過 null 條件運算符進行調用的,并且 null value?.Substring 似乎傳回了 null,但語言行為按您的想法進行。這簡化了對 PadRight 的調用過程,并立即傳回 null,進而避免會導緻出現 NullReferenceException 的程式設計錯誤。這個概念稱為“null 傳播”。
Null 條件運算符會根據具體條件進行 null 檢查,然後再調用目标方法以及調用鍊中的所有其他方法。這将可能産生一個令人驚訝的結果,例如,text?.Length.GetType 語句中的結果。
如果 null 條件運算符在調用目标為 null 時傳回 null,那麼調用會傳回值類型的成員時最終會是什麼資料類型(假定值類型不能為 null)?例如,從 value?.Length 傳回的資料類型不能隻是 int。答案當然是:可以為 null 的類型(int?)。實際上,嘗試僅将結果配置設定給 int 将會出現編譯錯誤:
int length = text?.Length; // Compile Error: Cannot implicitly convert type 'int?' to 'int'
Null 條件具有兩種文法形式。首先,問号在點運算符前面 (?.)。其次,将問号和索引運算符結合使用。例如,給定一個集合(而非在索引到集合之前顯式進行 null 檢查),您就可以使用 null 條件運算符執行此操作:
public static IEnumerable<T> GetValueTypeItems<T>(
IList<T> collection, params int[] indexes)
where T : struct
{
foreach (int index in indexes)
{
T? item = collection?[index];
if (item != null) yield return (T)item;
}
}
此示例使用了運算符 ?[…] 的 null 條件索引形式,導緻僅在集合不為 null 時才索引到集合。通過 null 條件運算符的此形式,T? item = collection?[index] 語句在行為上相當于:
T? item = (collection != null) ? collection[index] : null.
請注意,null 條件運算符僅可檢索項目,不會配置設定項目。如果給定 null 集合,那麼這意味着什麼?
請注意針對引用類型使用 ?[…] 時的隐式歧義。由于引用類型可以為 null,是以對于集合是否為 null,或者是否元素本身實際上就是 null 而言,來自 ?[…] 運算符的 null 結果不明确。
Null 條件運算符的一個非常有用的應用程式解決了 C# 自 C# 1.0 以來一直存在的的一個特性,即在調用委托之前檢查是否為 null。我們來看一下圖中顯示的 C# 2.0 代碼。
圖 1 在調用委托之前檢查是否為 Null
class Theremostat
{
event EventHandler<float> OnTemperatureChanged;
private int _Temperature;
public int Temperature
{
get
{
return _Temperature;
}
set
{
// If there are any subscribers, then
// notify them of changes in temperature
EventHandler<float> localOnChanged =
OnTemperatureChanged;
if (localOnChanged != null)
{
_Temperature = value;
// Call subscribers
localOnChanged(this, value);
}
}
}
}
通過使用 null 條件運算符,整個 set 實作過程就可簡化為:
OnTemperatureChanged?.Invoke(this, value)
現在,您隻需對将 null 條件運算符作為字首的 Invoke 進行調用,不再需要将委托執行個體配置設定給本地變量,進而實作線程安全,甚至是在調用委托之前顯式檢查值是否為 null。
C# 開發人員都很想知道在最新的四個版本中是否對此内容有所改進。答案是最終進行了改進。僅此一項功能就可以改變調用委托的方式。
另一個 null 條件運算符普及的常見模式是與 coalesce 運算符結合使用。您無需在調用 Length 之前對 linesOfCode 進行 null 檢查,而是可以編寫項目計數算法,如下所示:
List<string> linesOfCode = ParseSourceCodeFile("Program.cs");
return linesOfCode?.Count ?? 0;
在這種情況下,任何空集合(無項目)和 null 集合均标準化為傳回相同數量。總之,null 條件運算符将實作以下功能:
1. 如果操作數為 null,則傳回 null
2. 如果操作數為 null,則簡化調用鍊中的其他調用
3. 如果目标成員傳回一個值類型,則傳回可以為 null 的類型 (System.Nullable<T>)。
4. 以線程安全的方式支援委托調用
5. 可用作成員運算符 (?.) 和索引運算符 (?[…])
示例代碼下載下傳
引用
http://stackoverflow.com/questions/13629051/net-event-raising-and-nullobject-pattern ---線程安全的擴充機制
https://msdn.microsoft.com/zh-cn/magazine/dn802602.aspx ---C#6.0 Null條件運算符
http://en.wikipedia.org/wiki/Null_Object_pattern ---維基百科上的NullObjectPattern解釋
作者:Stephen Cui
出處:http://www.cnblogs.com/cuiyansong
版權聲明:文章屬于本人及部落格園共有,凡是沒有标注[轉載]的,請在文章末尾加入我的部落格位址。
如果您覺得文章寫的還不錯,請點選“推薦一下”,謝謝。