什麼是 Lazy<T>?
System.Lazy<T> 是 .NET Framework 4.0 引入(位於 System 命名空間)的泛型類,用於實現線程安全的延遲初始化(Lazy Initialization)。它確保一個昂貴的對象或資源只在第一次真正需要時才被創建,並且在多線程環境下保證初始化只發生一次。
-
核心特性:
- 延遲計算:值的創建被推遲到第一次訪問
.Value屬性時。 - 線程安全:內置多種線程安全模式,默認情況下完全線程安全。
- 異常緩存:如果初始化過程中拋出異常,後續訪問會重複拋出同一個異常(不重複執行工廠方法)。
- 值緩存:一旦初始化完成,後續所有訪問都返回同一個實例(單例行為)。
- 延遲計算:值的創建被推遲到第一次訪問
-
適用場景:
- 昂貴資源初始化(如數據庫連接、大文件加載、複雜計算)。
- 配置對象、單例模式(比雙檢查鎖更安全簡潔)。
- 避免啓動時不必要的開銷(尤其在某些代碼路徑可能不使用該對象時)。
為什麼使用 Lazy<T>?
傳統方式(如直接在字段初始化或手動雙檢查鎖)存在問題:
- 提前初始化:浪費資源。
- 手動鎖:易出錯(死鎖、競爭條件、
ABA問題)。 - 異常處理複雜:需手動緩存異常。
Lazy<T>性能優化:按需初始化。
- 線程安全:微軟高度優化,無鎖或輕量鎖實現。
- 代碼簡潔:一行代碼取代複雜的雙檢查鎖。
- 防
ABA:內部使用複合狀態 +CAS機制,徹底避免ABA問題。 完美解決這些問題:
如何使用 Lazy<T>?
核心語法與屬性
| 成員 | 作用 |
|---|---|
Lazy<T>() |
構造函數:默認使用T的無參構造創建對象,線程安全模式為ExecutionAndPublication |
Lazy<T>(Func<T> valueFactory) |
構造函數:自定義對象創建邏輯(支持傳參、複雜初始化) |
Lazy<T>(LazyThreadSafetyMode mode) |
構造函數:指定線程安全模式 |
Value |
核心屬性:首次訪問觸發對象初始化,後續訪問返回已創建的實例(只讀) |
IsValueCreated |
布爾屬性:判斷對象是否已完成初始化 |
基礎示例(默認構造)
using System;
using System.Threading;
// 模擬創建成本高的對象(如加載配置、連接數據庫)
public class ExpensiveObject
{
public ExpensiveObject()
{
Console.WriteLine("ExpensiveObject 開始初始化(耗時操作)...");
Thread.Sleep(2000); // 模擬2秒耗時初始化
Console.WriteLine("ExpensiveObject 初始化完成!");
}
public void DoWork() => Console.WriteLine("ExpensiveObject 執行業務邏輯...");
}
class LazyBasicDemo
{
static void Main()
{
Console.WriteLine("程序啓動,創建Lazy<ExpensiveObject>實例...");
// 此時僅創建Lazy<T>容器,ExpensiveObject並未實例化
Lazy<ExpensiveObject> lazyObj = new Lazy<ExpensiveObject>();
Console.WriteLine($"對象是否已創建:{lazyObj.IsValueCreated}"); // 輸出:False
// 首次訪問Value:觸發ExpensiveObject的構造函數
Console.WriteLine("\n=== 首次訪問Value ===");
lazyObj.Value.DoWork();
Console.WriteLine($"對象是否已創建:{lazyObj.IsValueCreated}"); // 輸出:True
// 再次訪問Value:直接返回已創建的實例,不再初始化
Console.WriteLine("\n=== 再次訪問Value ===");
lazyObj.Value.DoWork();
}
}
輸出結果:
程序啓動,創建Lazy<ExpensiveObject>實例...
對象是否已創建:False
=== 首次訪問Value ===
ExpensiveObject 開始初始化(耗時操作)...
ExpensiveObject 初始化完成!
ExpensiveObject 執行業務邏輯...
對象是否已創建:True
=== 再次訪問Value ===
ExpensiveObject 執行業務邏輯...
自定義工廠方法(帶參數初始化)
若對象需要傳參或複雜初始化邏輯,使用 Func<T> 工廠方法:
// 自定義帶參數的對象構造
public class ConfigObject
{
private string _configPath;
public ConfigObject(string configPath)
{
_configPath = configPath;
Console.WriteLine($"加載配置文件:{configPath}");
}
public string GetConfig() => $"配置內容(來自{_configPath})";
}
class LazyFactoryDemo
{
static void Main()
{
// 自定義工廠方法:創建ConfigObject並傳入參數
Lazy<ConfigObject> lazyConfig = new Lazy<ConfigObject>(() =>
{
Console.WriteLine("執行自定義工廠方法...");
return new ConfigObject("appsettings.json");
});
Console.WriteLine("首次訪問配置:");
Console.WriteLine(lazyConfig.Value.GetConfig()); // 觸發工廠方法
}
}
Lazy<T> 的線程安全模式
Lazy<T> 的關鍵特性是支持多線程場景,通過 LazyThreadSafetyMode 枚舉控制線程安全行為
| 線程安全模式 | 適用場景 | 核心行為 | 性能 |
|---|---|---|---|
None |
單線程環境 | 無線程安全保障,多線程同時訪問Value可能創建多個實例 |
最高(無鎖) |
PublicationOnly |
多線程 + 對象創建成本低 | 多線程可同時初始化,但最終僅保留一個實例(其他實例被丟棄),無鎖阻塞 | 高 |
ExecutionAndPublication(默認) |
多線程 + 對象創建成本高 | 加鎖保證只有一個線程執行初始化,其他線程等待,絕對單實例 |
示例:多線程下的線程安全模式
class LazyThreadSafetyDemo
{
// 默認模式:ExecutionAndPublication(加鎖保證單實例)
static Lazy<ExpensiveObject> lazySafeObj = new Lazy<ExpensiveObject>();
static void AccessLazyObj()
{
Console.WriteLine($"線程{Thread.CurrentThread.ManagedThreadId} 嘗試訪問Value...");
var obj = lazySafeObj.Value;
Console.WriteLine($"線程{Thread.CurrentThread.ManagedThreadId} 獲取對象HashCode:{obj.GetHashCode()}");
}
static void Main()
{
// 啓動5個線程同時訪問
for (int i = 0; i < 5; i++)
{
new Thread(AccessLazyObj).Start();
}
Thread.Sleep(3000); // 等待所有線程完成
}
}
輸出結果
所有線程最終獲取的 HashCode 完全相同,且僅觸發一次初始化(證明單實例):
線程3 嘗試訪問Value...
ExpensiveObject 開始初始化(耗時操作)...
線程4 嘗試訪問Value...
線程5 嘗試訪問Value...
線程6 嘗試訪問Value...
線程7 嘗試訪問Value...
ExpensiveObject 初始化完成!
線程3 獲取對象HashCode:46104728
線程4 獲取對象HashCode:46104728
線程5 獲取對象HashCode:46104728
線程6 獲取對象HashCode:46104728
線程7 獲取對象HashCode:46104728
高級應用場景
懶加載單例模式
Lazy<T> 是 .NET 中實現線程安全、懶加載單例的最優方式:
public sealed class LazySingleton
{
// 私有靜態Lazy實例:延遲初始化,默認線程安全
private static readonly Lazy<LazySingleton> _lazyInstance = new Lazy<LazySingleton>(() => new LazySingleton());
// 私有構造函數:禁止外部實例化
private LazySingleton()
{
Console.WriteLine("單例對象初始化...");
}
// 公共屬性:首次訪問時創建實例
public static LazySingleton Instance => _lazyInstance.Value;
public void DoBusiness() => Console.WriteLine("單例對象執行業務邏輯...");
}
// 使用
class SingletonDemo
{
static void Main()
{
Console.WriteLine("程序啓動,未訪問單例...");
// 首次訪問Instance:觸發單例初始化
LazySingleton.Instance.DoBusiness();
// 再次訪問:直接返回已創建的實例
LazySingleton.Instance.DoBusiness();
}
}
處理初始化異常
若 Lazy<T> 的工廠方法拋出異常,後續訪問 Value 會緩存並重新拋出同一異常
Lazy<ExpensiveObject> lazyErrorObj = new Lazy<ExpensiveObject>(() =>
{
throw new InvalidOperationException("初始化失敗:配置文件不存在!");
});
try
{
// 首次訪問:拋出異常
lazyErrorObj.Value.DoWork();
}
catch (AggregateException ex)
{
Console.WriteLine($"初始化異常:{ex.InnerException.Message}");
}
try
{
// 再次訪問:仍拋出同一異常(異常被緩存)
lazyErrorObj.Value.DoWork();
}
catch (AggregateException ex)
{
Console.WriteLine($"再次訪問異常:{ex.InnerException.Message}");
}
異步懶加載(偽 AsyncLazy)
private readonly Lazy<Task<ExpensiveObject>> _asyncResource = new(async () =>
{
await Task.Delay(2000);
return new ExpensiveObject();
});
public async Task<ExpensiveObject> GetResourceAsync() => await _asyncResource.Value;
支持刷新(Reset)的自定義包裝
public class RefreshableLazy<T>
{
private Lazy<T> _current;
public RefreshableLazy(Func<T> factory)
{
_current = new Lazy<T>(factory);
}
public T Value => _current.Value;
public void Refresh()
{
var factory = _current.Value; // 保留舊工廠?或傳入新工廠
_current = new Lazy<T>(_current.Factory); // 重新創建
}
}
與依賴注入結合(ASP.NET Core)
services.AddSingleton<ExpensiveService>();
services.AddSingleton<Lazy<ExpensiveService>>(provider =>
new Lazy<ExpensiveService>(provider.GetRequiredService<ExpensiveService>));
配置文件加載
public class AppConfig
{
private static readonly Lazy<AppConfig> _instance =
new Lazy<AppConfig>(LoadConfiguration);
public static AppConfig Instance => _instance.Value;
public string ConnectionString { get; }
public int Timeout { get; }
private AppConfig(string config)
{
// 解析配置
}
private static AppConfig LoadConfiguration()
{
string configData = File.ReadAllText("config.json");
return new AppConfig(configData);
}
}
Lazy<T> vs 手動懶加載
手動實現線程安全的懶加載需要寫雙重檢查鎖定(易出錯),而 Lazy<T> 已封裝所有邏輯:
// 手動實現(線程安全,需雙重檢查+volatile)
private volatile ExpensiveObject _manualObj;
private readonly object _lockObj = new object();
public ExpensiveObject ManualObj
{
get
{
if (_manualObj == null)
{
lock (_lockObj)
{
if (_manualObj == null)
{
_manualObj = new ExpensiveObject();
}
}
}
return _manualObj;
}
}
// Lazy<T>實現(一行搞定,線程安全)
private readonly Lazy<ExpensiveObject> _lazyObj = new Lazy<ExpensiveObject>();
public ExpensiveObject LazyObj => _lazyObj.Value;
Lazy<T> 內部實現原理(簡要)
-
使用一個私有字段(如
object? _box)存儲狀態:null:未初始化Box<T>:已完成(包含值)- 異常對象:初始化失敗
- 初始化時使用
Interlocked.CompareExchange進行原子狀態轉換。 - 內部狀態機結合版本機制,徹底防止
ABA問題。 ExecutionAndPublication模式下使用輕量自旋 + 等待機制,確保只有一個線程執行工廠方法。
優點與缺點
| 方面 | 優點 | 缺點 |
|---|---|---|
| 線程安全 | 內置完美支持,防 ABA、防重複初始化 | None 模式下需手動注意 |
| 易用性 | 代碼極簡,一行搞定 | 不支持主動 Reset(需自定義包裝) |
| 性能 | 高度優化,無鎖路徑極快 | 第一次訪問可能阻塞(但這是延遲初始化的本質) |
| 功能 | 異常緩存、值緩存、IsValueCreated 查詢 | 不支持異步初始化(需用 Lazy<Task<T>>) |
| 適用性 | 幾乎所有懶加載場景的首選 | 若需支持刷新/重置,需額外封裝 |
最佳實踐
- 優先使用
Lazy<T>替代手動雙檢查鎖。 - 始終使用默認線程安全模式(除非明確需要
PublicationOnly)。 - 工廠方法應無副作用(尤其在
PublicationOnly模式下)。 - 異常處理:捕獲
.Value拋出的異常。 - 不要在工廠方法中訪問同一個
Lazy實例(可能死鎖)。
總結
System.Lazy<T>是.NET官方的延遲初始化工具,核心是首次訪問Value時創建對象,提升程序啓動速度和內存效率;- 核心屬性:
Value(觸發初始化)、IsValueCreated(判斷是否已創建); - 線程安全模式需按需選擇:單線程用
None,多線程高成本對象用默認的ExecutionAndPublication; - 最優場景:創建成本高 / 不一定使用的對象、懶加載單例模式;