Lazy的Value属性在首次读取返回值时才执行工厂函数,且默认线程安全仅保证工厂函数最多执行一次。

Lazy
Lazy 的 Value 属性到底什么时候执行工厂函数?
不是声明 new Lazy<t>()</t> 时,也不是访问 Value 属性的 getter 本身,而是第一次读取 Value 的返回值那一刻才真正调用工厂函数。这个“第一次”是线程安全的(默认模式下),多个线程同时触发也只执行一次。
- 常见误判:
new Lazy<foo>(() => new Foo())</foo>中的new Foo()在构造Lazy<foo></foo>时不执行,它被包裹在Func<foo></foo>委托里,压根没跑 - 如果工厂函数抛出
InvalidOperationException,该异常会被缓存;后续每次读Value都 rethrow 同一个异常实例,不会重试也不会新建 - 想验证是否真延迟?在工厂函数里加
Console.WriteLine("init!"),只会在首次Value访问时输出
为什么 new Lazy(func) 默认线程安全,但仍有并发风险?
Lazy<t></t> 的线程安全仅保证「工厂函数最多执行一次」,不保护工厂函数内部逻辑的线程安全性。比如你在工厂里往静态字典写数据,依然要自己加锁或换 ConcurrentDictionary。
- 默认使用
LazyThreadSafetyMode.ExecutionAndPublication:严格单次执行 + 单次发布,适合绝大多数场景 - 设为
LazyThreadSafetyMode.None后,若多线程并发首次读Value,可能创建多个实例,且无法预测哪个被最终返回——这不是 bug,是设计使然 - 设为
LazyThreadSafetyMode.PublicationOnly:允许多个线程各自初始化,但只保留第一个成功完成的结果;适合初始化成本低、结果等价的场景(如读配置) - 禁用同步(
isThreadSafe: false或None)只应在明确单线程上下文(如控制台主流程、单元测试 setup)中使用
Lazy 和属性自动初始化、字段直接 new 有什么本质区别?
区别在于「时机」:自动初始化(如 private readonly Foo _f = new Foo();)和字段直接 new 都发生在类实例化时;而 Lazy<t></t> 是在首次访问 Value 时才触发。
- 错误写法:
public Lazy<list>> Items => new Lazy<list>>(LoadItems);</list></list>—— 每次 get 属性都新建一个Lazy<t></t>实例,完全失去延迟意义 - 正确写法:
private readonly Lazy<list>> _items = new Lazy<list>>(LoadItems); public List<string> Items => _items.Value;</string></list></list> - 别对值类型(
int、DateTime)用Lazy<t></t>:堆分配 + 同步开销远超收益 - 别把它当缓存用:
Lazy<t></t>只管初始化一次,不响应外部状态变化;要动态更新得靠手动重置或换IObservable<t></t>
异步初始化不能直接用 Lazy,那该怎么写?
Func<t></t> 不接受 async 方法,所以 new Lazy<task>>(() => GetAsync())</task> 是错的——它会同步等待,可能死锁(尤其在 UI 或 ASP.NET 同步上下文里)。
- 正确姿势是封装
Task<t></t>:private readonly Lazy<task>> _data = new Lazy<task>>(LoadDataAsync); public async Task<string> GetDataAsync() => await _data.Value;</string></task></task> - 此时
_data.Value返回的是同一个Task<string></string>实例,不会重复发起请求,这才是关键价值 - 别用
Task.Run(() => new Lazy<t>())</t>:绕路启线程、丢失上下文、增加调度开销 - 需要真正异步延迟 + 线程安全 + 异常重试?考虑第三方库如
Microsoft.VisualStudio.Threading.AsyncLazy<t></t>或手写带取消/重试的包装
最容易被忽略的一点:Lazy


















