lazy的value属性在首次读取返回值时才执行初始化,严格保证线程安全,默认模式为executionandpublication,异常原样抛出且不重试,不适用于动态刷新场景。

Lazy
Lazy 的 Value 属性什么时候真正执行初始化?
不是声明时,不是构造 Lazy<t></t> 实例时,也不是访问 Value 的 getter 本身——而是在第一次读取 Value 的返回值时,才执行传入的 Func<t></t> 委托。
- 写成
new Lazy<foo>(() => new Foo())</foo>是对的;写成var f = new Foo(); new Lazy<foo>(() => f)</foo>就完全失效,f已在上一行被创建 - 如果工厂方法里抛异常(比如 DB 连接失败),这个异常会在首次访问
Value时原样抛出,且后续每次访问都重抛——Lazy<t></t>不重试、不缓存失败状态 -
IsValueCreated是只读属性,仅用于观察状态,不能用来“预热”或“试探性初始化”
为什么默认线程安全模式是 ExecutionAndPublication?
因为绝大多数场景下,你既不希望多个线程重复初始化(浪费资源),也不希望某个线程拿到未完成初始化的中间态对象(导致 NullReferenceException 或部分构造对象)。
- 默认模式
LazyThreadSafetyMode.ExecutionAndPublication:严格保证只有一个线程执行工厂方法,其余线程阻塞等待结果 - 设为
LazyThreadSafetyMode.None后,若多个线程同时首次访问Value,可能创建多个实例,且无法预测哪个被最终返回——这不是 bug,是设计行为 -
LazyThreadSafetyMode.PublicationOnly适合初始化极快、结果等价的场景(如 new Random()),但不适用于依赖外部状态(如配置、DB)的初始化
异步操作怎么和 Lazy 配合?
别直接套 Lazy<task>></task> 然后 await _lazy.Value ——这会把未完成的 Task 当作结果返回,丢失 await 语义。
- 正确做法是封装成
Lazy<task>></task>,但调用方必须显式 await:await _data.Value;此时_data.Value返回的是同一个Task实例,不会重复发起请求 - 绝对不要写
Task.Run(() => new Lazy<t>(...))</t>:这绕过Lazy的上下文捕获能力,还额外起线程,纯属负优化 - 如果初始化逻辑本身要 await(比如读配置文件),工厂委托必须是
Func<task>></task>,但Lazy<t></t>不支持 async/await 委托——得自己包一层,或改用AsyncLazy<t></t>(需自行实现或引用第三方库如 Microsoft.VisualStudio.Threading)
哪些地方最容易踩坑?
最隐蔽的问题往往不在初始化逻辑本身,而在生命周期和引用管理上。
- 工厂委托里捕获了大对象(如整个
IServiceProvider或长生命周期服务),会导致该对象无法被 GC 回收,形成内存泄漏 - 把
Lazy<t></t>声明为静态字段又没控制好初始化时机,可能在类型初始化阶段就触发副作用(比如提前连 DB) - 误以为
Lazy<t></t>能解决“对象更新”问题——它只管“首次创建”,不负责后续刷新;需要动态刷新请用Lazy<t></t>+ 显式 Reset 逻辑,或换用其他模式(如策略模式 + 缓存失效)
真正关键的不是“怎么写”,而是想清楚:这个对象是否真的“可能永不使用”?它的初始化成本是否高到值得加一层间接?以及,它的依赖项是否在首次访问 Value 时一定已就绪?这三个问题没想清,Lazy<t></t> 很容易变成技术债。










