在軟件開發中,術語延遲指的是盡可能久地推遲特定的高開銷活動的空閒時 間。軟件延遲過程中其實也在進行操作,但意味著任何操作僅當需要完成某一 特定任務時才會發生。就這一點而言,延遲是軟件開發中的一種重要模式,可 以成功地應用於包括設計與實施在內的各種情景中。
例如,極限編程方法中的一種基本編碼實踐就被簡單地概括為“您不會需要 它”,那就是一種明確的延遲要求 - 當且僅當您需要這些功能時,才需要在基 本代碼中包含它。
從另一個角度來看,在實施類的過程中,當要從難以訪問的源中加載數據時 ,您也可能需要延遲。事實上,延遲加載模式解釋了這種普遍接受的解決方案 ,即定義一個類成員,但使其保持為空,直到其他某些客戶端代碼實際需要其內 容時為止。延遲加載完全適合在對象關系映射 (ORM) 工具(如實體框架和 NHibernate)環境中使用。ORM 工具用於映射面向對象的環境與關系數據庫之 間的數據結構。例如,在這種環境中,延遲加載指的就是僅當某些代碼嘗試讀 取 Customer 類上公開的 Orders 集合屬性時,
框架才能加載 Customer 的 Orders。
但是,延遲加載並不限於特定的實施方案(如 ORM 編程)。而且,延遲加 載指的就是在某些數據實際可用之前不獲取該數據的實例。換言之,延遲加載 就是要有特殊工廠邏輯,即跟蹤必須要創建的內容,最後在實際請求該內容時以 靜默方式創建該內容。
在 Microsoft .NET Framework 中,開發人員早就在我們的類中手動實施了 所有延遲行為。在 .NET Framework 4 問世之前,從未有過內置的機制來幫助 完成此任務。在 .NET Framework 4 中,我們可以開始
使用全新的 Lazy<T> 類。
了解 Lazy<T> 類
Lazy<T> 是一個特殊的工廠,您可以用來包裝給定 T 類型的對象。Lazy<T> 包裝代表一個尚不存在的類實例的實時代理。使用 Lazy 包裝 的理由有很多,其中最重要的莫過於可以提高性能。延遲初始化對象可以避免 所有不必要的計算,從而減少內存消耗。如果加以合理利用,延遲初始化對象 也可以成為一種加快應用程序啟動的強大工具。以下代碼說明了以延遲方式初 始化對象的方法:
var container = new Lazy<DataContainer>();
在本例中,DataContainer 類表示的是一個引用了其他對象數組的純數據容 器對象。在剛剛對 Lazy<T> 實例調用完 new 運算符之後,返回的只是 一個實時的 Lazy<T> 類實例;無論如何都不會得到指定類型 T 的實例。如果您需要向其他類的成員傳遞一個 DataContainer 實例,則必須更改這些成 員的簽名才能使用 Lazy<DataContainer>,如下所示:
void ProcessData(Lazy<DataContainer> container);
何時創建 DataContainer 的實際實例,以便程序可以處理其所需的數據? 讓我們來看看 Lazy<T> 類的公共編程接口。該公共接口非常小,因為它 只包含兩個屬性:Value 和 IsValueCreated。如果存在與 Lazy 類型關聯的實 例,則屬性 Value 就會返回該實例的當前值。該屬性的定義如下:
public T Value
{
get { ... }
}
屬性 IsValueCreated 可以返回一個 Boolean 值,表示 Lazy 類型是否已經 過實例化。以下是該屬性的源代碼中的一段摘錄:
public bool IsValueCreated
{
get
{
return ((m_boxed != null) && (m_boxed is Boxed<T>));
}
}
如果 Lazy<T> 類包含 T 類型的實際實例(如果有),則 m_boxed 成 員就是該類的一個內部私有的不穩定成員。因此,IsValueCreated 只需檢查是 否存在 T 的實時實例,然後返回一個 Boolean 答案。如前文所述,m_boxed 成員是私有的並且不穩定(如以下代碼段所示):
private volatile object m_boxed;
在 C# 中,volatile 關鍵字表示成員可以被並發運行的線程修改。volatile 關鍵字用於下面這樣的成員:這類成員可以在多線程環境中使用,但 無法防止多個可能的並發線程同時對其進行訪問(本意是出於性能因素考慮)。我們稍後再回到 Lazy<T> 的線程方面上來。目前,可以肯定地說,默認 情況下 Lazy<T> 的公共成員和受保護成員是線程安全的。當有任意代碼 首次嘗試訪問 Value 成員時,就會創建類型 T 的實際實例。對象創建方面的 詳細信息取決於各種線程屬性,這些屬性可以通過 Lazy<T> 構造函數來 指定。應該明確的是,線程模式的含義僅當 boxed 值實際上已初始化或首次被 訪問時才很重要。
默認情況下,類型 T 的實例是通過調用 Activator.CreateInstance 進行反 射獲取的。以下是一個典型的與 Lazy<T> 類型進行交互的簡單示例:
var temp = new Lazy<DataContainer>();
Console.WriteLine(temp.IsValueCreated);
Console.WriteLine(temp.Value.SomeValue);
請注意,在調用 Value 之前,並不一定要對 IsValueCreated 進行檢查。通常情況下,僅當(無論出於何種原因)您需要了解某個值當前是否與 Lazy 類 型關聯時,才必須查看 IsValueCreated 的值。您無需檢查 IsValueCreated 即可避免發生對 Value 的空引用異常。以下代碼即可保證正常運行:
var temp = new Lazy<DataContainer>();
Console.WriteLine(temp.Value.SomeValue);
Value 屬性的 getter 會檢查 boxed 值是否已經存在;如果不存在,則會觸 發邏輯創建一個包裝類型實例,並返回該實例。
實例化過程
當然,當該 Lazy 類型(上例中的 DataContainer)的構造函數引發異常時 ,您的代碼會負責處理該異常。所捕獲異常屬於 TargetInvocationException 類型,該異常是 .NET 反射無法間接創建某類型實例時收到的典型異常。
Lazy<T> 包裝邏輯只能確定是否已創建類型 T 的實例,並不能保證您 在訪問 T 上的任意公共成員時都不會收到空引用異常。以下面的代碼段為例:
public class DataContainer
{
public DataContainer()
{
}
public IList<String> SomeValues { get; set; }
}
現在假設您嘗試從客戶端程序調用以下代碼:
var temp = new Lazy<DataContainer>();
Console.WriteLine(temp.Value.SomeValues.Count);
在這種情況下,您將收到一個異常,這是因為 DataContainer 對象的 SomeValues 屬性為空,而非 DataContainer 本身為空。引發該異常是因為 DataContainer 的構造函數沒有正常初始化其所有成員;該錯誤與 lazy 方法的 實施無關。
Lazy<T> 的 Value 屬性為只讀屬性,即一旦經過初始化, Lazy<T> 對象將始終返回同一個類型 T 實例或同一個值(當 T 為值類型 時)。您無法修改實例,但可以訪問該實例可能擁有的所有公共屬性。
以下是配置 Lazy<T> 對象向 T 類型傳遞臨時參數的方法:
temp = new Lazy<DataContainer>(() => new Orders(10));
其中一個 Lazy<T> 構造函數會接受一個委托,您可以通過該委托指定 為 T 構造函數產生正確輸入數據所需的任何操作。在首次訪問包裝的 T 類型 的 Value 屬性之前,不會運行該委托。
線程安全初始化
默認情況下,Lazy<T> 是線程安全的,即多個線程可以訪問一個實例 ,並且所有線程都會收到同一個T 類型的實例。讓我們來看看線程方面的內容 ,線程僅在首次訪問 Lazy 對象時很重要。
第一個訪問 Lazy<T> 對象的線程將觸發類型 T 的初始化過程。所有 後續獲得 Value 的訪問權限的線程都會收到第一個線程(無論什麼線程)生成 的響應。換言之,如果第一個線程在調用類型 T 的構造函數時引發了異常,則 所有後續調用(無論什麼線程)都會收到同樣的異常。
按照設計,不同的線程無法從同一個 Lazy<T> 實例獲得不同的響應。這是您選擇默認的 Lazy<T> 構造函數時獲得的行為。
但是,Lazy<T> 類也可以運行另一個構造函數:
public Lazy(bool isThreadSafe)
Boolean 參數表示您是否需要線程安全。如前文所述,默認值為 true,就 表示可以提供上述行為。
但是,如果您傳遞的是 false,則將只從一個線程(初始化該 Lazy 類型的 線程)訪問 Value 屬性。未定義當有多個線程嘗試訪問 Value 屬性時的行為 。
接受 Boolean 值的 Lazy<T> 構造函數是一種更常見簽名的特殊情況 ,在這種情況下,您要通過 LazyThreadSafetyMode 枚舉向 Lazy<T>
構造函數傳遞值。圖 1 說明了該枚舉中每個值的作用。
圖 1 LazyThreadSafetyMode 枚舉
值 說明 None Lazy<T> 實例不是線程安全的,並且未定義當從多個線程訪問 該實例時的行為。 PublicationOnly 允許多個線程同時嘗試初始化 Lazy 類型。第一個完成的線程是獲 勝者,所有其他線程生成的結果都將被丟棄。 ExecutionAndPublication 為了確保只有一個線程能夠以線程安全的方式初始化 Lazy<T> 實例而使用了鎖。
您可以使用以下任一構造函數來設置 PublicationOnly 模式:
public Lazy(LazyThreadSafetyMode mode)
public Lazy<T>(Func<T>, LazyThreadSafetyMode mode)
圖 1 中除 PublicationOnly 以外的值都是在使用接受 Boolean 值的構造函 數時隱式設置的:
public Lazy(bool isThreadSafe)
在該構造函數中,如果參數 isThreadSafe 為 false,則選定的線程模式為 None。如果參數 isThreadSafe 設置為 true,則線程模式設置為 ExecutionAndPublication。ExecutionAndPublication 也是您選擇默認構造函 數時的工作模式。
使用 ExecutionAndPublication 時可以保證完全線程安全,使用 None 時缺 乏線程安全,而使用 PublicationOnly 模式則介於二者之間。PublicationOnly 允許多個並發線程嘗試創建類型 T 實例,但只允許一個線程 是獲勝者。獲勝者創建的 T 實例隨後會在所有其他線程(無論每個線程計算的 實例如何)之間共享。
就初始化過程中可能引發異常方面,None 和 ExecutionAndPublication 之 間有一個很有趣的區別。當設置為 PublicationOnly 且初始化過程中產生的異 常未寫入緩存時,如果 T 實例不可用,則嘗試讀取 Value 的每個後續線程都有 機會重新初始化該實例。PublicationOnly 和 None 之間的另一個區別是,當 T 的構造函數嘗試遞歸訪問 Value 時,PublicationOnly 模式中不會引發任何 異常。當 Lazy<T> 類以 None 或 ExecutionAndPublication 模式工作 時,該情況會引發 InvalidOperation 異常。
放棄線程安全可以獲得原有的性能優勢,但要注意防止出現令人討厭的 Bug 和爭用情況。因此,建議您僅當性能極為關鍵時才使用 LazyThreadSafetyMode.None 選項。
使用 LazyThreadSafetyMode.None 時,您需要負責確保絕不會發生從多個線 程對 Lazy<T> 實例進行初始化的情況。否則,可能會產生不可預料的結 果。如果初始化過程中引發異常,則對於該線程,對 Value 的所有後續訪問都 會緩存和引發相同的異常。
ThreadLocal 初始化
按照設計,Lazy<T> 禁止不同的線程管理其各自的類型 T 實例。但 是,如果您希望允許該行為,您必須選擇其他類(ThreadLocal<T> 類型 )。以下是該類的使用方法:
var counter = new ThreadLocal<Int32>(() => 1);
構造函數會接受一個委托,並使用該委托來初始化 thread-local 變量。每 個線程都會保留自己的數據,其他線程完全無法訪問該數據。與 Lazy<T> 不同,ThreadLocal<T> 上的 Value 屬性是可讀寫的。因此,每個訪問與下一個訪問之間是獨立的,可能產生包括引發(或不引發)異 常在內的不同結果。如果您未通過 ThreadLocal<T> 構造函數提供操作 委托,則嵌入的對象將使用該類型的默認值 null(當 T 為一個類時)進行初始 化。
實現 Lazy 屬性
大多數情況下,您要使用 Lazy<T> 作為您自己的類中的屬性,但到底 是哪些類中要使用它呢? ORM 工具本身提供了延遲加載功能,因此如果您使用 的是這些工具,在數據訪問層所在的應用程序片段中很可能找不到可能承載 lazy 屬性的候選類。如果您使用的不是 ORM 工具,則數據訪問層肯定非常適 合 lazy 屬性。
可以在其中使用依賴關系注入的應用程序片段也可能非常適合延遲。在 .NET Framework 4 中,托管可擴展性框架 (MEF) 只使用 Lazy<T> 來實 現控件的可擴展性和反轉。即使您不是直接使用 MEF,依賴關系的管理也非常 適合 lazy 屬性。
在類中實現 lazy 屬性並不困難,如圖 2 所示。
圖 2 Lazy 屬性示例
public class Customer
{
private readonly Lazy<IList<Order>> orders;
public Customer(String id)
{
orders = new Lazy<IList<Order>>( () =>
{
return new List<Order>();
}
);
}
public IList<Order> Orders
{
get
{
// Orders is created on first access
return orders.Value;
}
}
}
補充說明
總而言之,延遲加載是一個抽象的概念,指的是僅當真正需要數據時才加載 數據。在 .NET Framework 4 問世之前,開發人員需要自己開發延遲初始化邏 輯。Lazy<T> 類擴展了 .NET Framework 編程工具包,可讓您在當且僅 當嚴格需要高開銷對象時,才在恰好開始使用這些對象之前對這些對象進行實例 化,從而避免浪費計算資源。