lazy 的 value 属性仅在首次访问时触发初始化,之后始终返回已创建实例;其线程安全默认为 executionandpublication 模式,但工厂函数内部仍需自行保证线程安全。

Lazy 的 Value 属性到底什么时候触发初始化
它只在第一次访问 Value 属性时执行工厂函数,之后所有访问都直接返回已创建的对象实例——哪怕你反复读取一百次,也只初始化一次。
常见错误是误以为 new Lazy<t>()</t> 就会立即执行构造或工厂逻辑,其实不会;IsValueCreated 为 false 是正常状态,直到你真正读取 Value 才会变 true。
- 不要在构造函数里提前调用
Value,否则失去延迟意义 - 避免把
Value存入局部变量后反复使用——这不是错,但容易掩盖“首次访问才初始化”的语义 - 调试时注意:断点停在
Valuegetter 内部,才能看到初始化过程;只看变量监视器可能显示null或未求值状态
多线程环境下 Lazy 默认是否安全
是,默认线程安全模式是 LazyThreadSafetyMode.ExecutionAndPublication,即多个线程同时首次访问 Value,也只执行一次工厂函数,其余线程阻塞等待结果并共享该实例。
但要注意:这个“安全”仅指初始化过程不重复执行,不保证你工厂函数内部的代码线程安全。比如你在工厂里操作静态集合、写文件、改全局状态,仍需自行加锁或隔离。
- 若确定单线程场景(如 UI 线程独占),可用
LazyThreadSafetyMode.None省掉锁开销 -
LazyThreadSafetyMode.PublicationOnly允许多次执行工厂函数,但只发布第一个成功结果——适合能容忍少量冗余计算的场景 - 别手动用
lock包裹Value访问,这反而破坏 Lazy 的设计契约,还可能引发死锁
异步初始化不能直接用 Lazy>
直接写 Lazy<task>></task> 是常见误区。它确实能延迟创建 Task 实例,但 Value 返回的是一个未 await 的 Task,不是最终结果;而且多次访问 Value 会拿到同一个 Task 实例——这点是对的,但容易让人忽略 await 的必要性。
正确做法是封装成同步风格的懒加载 + 异步执行,例如:
private readonly Lazy<task>>> _data = new Lazy<task>>>(LoadDataAsync); public async Task<list>> GetDataAsync() => await _data.Value;</list></task></task>
关键点在于:工厂函数返回 Task,_data.Value 拿到的是那个已启动(或已缓存完成)的 Task,await 它才得到真实数据。
- 别写
Task.Run(() => new Lazy<t>(...))</t>,这既绕过 Lazy 的线程安全机制,又丢失 async/await 上下文捕获能力 - 如果工厂函数本身是异步的(比如含
await),必须用Func<task>></task>类型,不能用Func<t></t> -
Lazy<task>></task>的IsValueCreated只表示 Task 是否已创建,不代表 Task 是否已完成
Lazy 和单例模式混用时最容易漏掉什么
用 Lazy<t></t> 实现懒汉单例很常见,但很多人忘了给 Lazy<t></t> 的工厂函数加 readonly 修饰或确保其无副作用——因为单例本该是稳定、可预测的。
更隐蔽的问题是:如果工厂函数抛出异常,Lazy<t></t> 会缓存该异常,后续每次访问 Value 都重新抛出,而不是重试。这对单例来说往往不可接受。
- 务必在工厂函数内做异常兜底,或用
TryGetValue+ 重试逻辑包装 - 不要在工厂中依赖随时可能失效的外部状态(如配置未加载完、网络未就绪),否则单例初始化会永久失败
- 若单例需要 Dispose,记得暴露
Dispose方法并管理内部资源,Lazy<t></t>本身不处理生命周期
Lazy<t></t> 不提供重试、超时、日志钩子等能力,这些得靠你自己补全。










