Lazy<T> 通用使用指南
Lazy<T> 是 .NET 内置的**延迟初始化(Lazy Initialization)**工具类,位于 System 命名空间。它的核心思想是:对象的创建被推迟到第一次真正被访问时才执行,并且保证只创建一次、线程安全。
一、核心概念
csharp
// 1. 声明一个"延迟容器",此刻并未创建 T 的实例
Lazy<MyService> lazy = new(() => new MyService());
// 2. 第一次访问 .Value 时才真正创建,之后复用缓存
MyService service = lazy.Value;
关键点:
| 概念 | 说明 |
|---|---|
| 延迟 | 工厂委托(Func<T>)只在第一次访问 .Value 时执行 |
| 缓存 | 创建结果被内部缓存,后续访问直接返回,不重复创建 |
| 线程安全 | 默认模式下,多线程同时首次访问时只有一个线程执行工厂,其余等待复用 |
二、构造函数与线程安全模式
Lazy<T> 有多个构造函数重载,最常用的是:
csharp
// 方式一:无参(要求 T 有无参构造函数)
Lazy<MyService> a = new();
// 方式二:传入工厂委托(最常用,可自定义创建逻辑)
Lazy<MyService> b = new(() => new MyService("config"));
// 方式三:指定线程安全模式
Lazy<MyService> c = new(() => new MyService(), LazyThreadSafetyMode.ExecutionAndPublication);
三种 LazyThreadSafetyMode 对比:
| 模式 | 线程安全 | 工厂执行次数 | 适用场景 |
|---|---|---|---|
ExecutionAndPublication(默认) |
✅ 完全线程安全 | 仅 1 次 | 绝大多数场景,推荐 |
PublicationOnly |
✅ 发布安全 | 可能多次(取先完成者) | 工厂很慢、可接受重复计算、结果可丢弃 |
None |
❌ 不保证 | 可能多次 | 明确单线程访问,追求极致性能 |
注意:
PublicationOnly下工厂可能被多个线程各执行一次,但最终只有一个结果被"发布";None模式下多线程访问会出问题,除非你确定只有单线程访问。
三、使用场景
场景 1:单例模式(最经典)
保证全局唯一实例,且首次访问才创建:
csharp
public sealed class ConfigManager
{
private static readonly Lazy<ConfigManager> _instance = new(() => new ConfigManager());
public static ConfigManager Instance => _instance.Value;
private ConfigManager()
{
// 加载配置...
}
}
优点:比手写双检锁(Double-Check Locking)更简洁,线程安全由框架保证。
场景 2:昂贵资源的按需加载
对象创建成本高(IO、网络、反射、大对象),但可能根本用不到:
csharp
public class ReportService
{
// 数据库连接池、大字典、正则表达式等,用到才创建
private readonly Lazy<Regex> _emailRegex = new(() => new Regex(
@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.Compiled));
private readonly Lazy<Dictionary<string, string>> _lookupTable = new(() =>
{
var dict = new Dictionary<string, string>();
// 从文件/数据库加载大量数据...
return dict;
});
public bool IsValidEmail(string email) => _emailRegex.Value.IsMatch(email);
}
优点:程序启动快,资源真正被用到时才付出创建成本。
场景 3:静态字段的延迟初始化
静态字段在类首次被引用时就会初始化,用 Lazy<T> 可推迟到真正访问:
csharp
public static class AppInfo
{
// 不访问就不执行,避免无谓的反射/IO
private static readonly Lazy<Version> _version = new(() =>
Assembly.GetExecutingAssembly().GetName().Version!);
public static Version Version => _version.Value;
}
场景 4:避免循环依赖 / 初始化顺序问题
两个对象互相引用时,用 Lazy<T> 打破初始化顺序的耦合:
csharp
public class ServiceA
{
public Lazy<ServiceB> B { get; } = new(() => new ServiceB());
}
public class ServiceB
{
public Lazy<ServiceA> A { get; } = new(() => new ServiceA());
}
场景 5:属性级缓存(Memoization)
把"计算一次、多次复用"的逻辑封装成属性:
csharp
public class Order
{
private readonly Lazy<decimal> _total = new(() =>
{
// 复杂的累加计算,只算一次
return Items.Sum(i => i.Price * i.Quantity);
});
public decimal Total => _total.Value;
}
场景 6:依赖注入容器 / 服务定位器
在简易 IoC 容器中,用 Lazy<T> 实现"按需创建 + 单例缓存":
csharp
public class SimpleContainer
{
private readonly Dictionary<Type, Lazy<object>> _services = new();
public void Register<TService>(Func<TService> factory) where TService : class
{
_services[typeof(TService)] = new Lazy<object>(() => factory()!);
}
public TService Resolve<TService>() where TService : class
{
return (TService)_services[typeof(TService)].Value;
}
}
四、注意事项
- 默认用
ExecutionAndPublication:除非有明确理由,不要轻易改成None或PublicationOnly。 - 工厂委托里不要有副作用 :工厂可能(在
PublicationOnly下)被多次执行,副作用代码应避免放在里面。 - 异常处理 :默认模式下,如果工厂抛出异常,
Lazy<T>会缓存异常,后续访问会再次抛出同一个异常 (不会重试)。如需重试,可考虑PublicationOnly或自行处理。 IsValueCreated属性:可判断实例是否已创建,用于调试或条件逻辑。- 不要与
async混用 :Lazy<T>的工厂是同步的Func<T>,无法直接await。需要异步延迟初始化时,考虑Lazy<Task<T>>:
csharp
private readonly Lazy<Task<HeavyData>> _data = new(async () =>
{
await Task.Delay(100); // 模拟异步加载
return new HeavyData();
});
// 使用:await _data.Value;
Lazy<T>与Lazy<Task<T>>的区别:前者缓存的是"创建好的对象",后者缓存的是"一个 Task",适合异步场景。
五、总结
Lazy<T> = 延迟创建 + 只创建一次 + 线程安全 三合一。凡是遇到"对象创建成本高、可能用不到、但需要全局唯一或复用"的场景,都可以优先考虑它------尤其是单例模式,它是最简洁、最不易出错的实现方式。