Lazy在首次访问Value属性时才真正创建实例,且默认线程安全,仅初始化一次。其构造函数接收Func委托,new表达式必须置于委托内,否则失去延迟加载意义。

Lazy 什么时候才真正创建实例?
不是声明 Lazy<t></t> 的那一刻,也不是访问 Value 属性的 getter 本身,而是第一次读取 Value 时才执行构造逻辑。这个“第一次”是线程安全的——即使多个线程同时首次访问,也只初始化一次。
常见误判:以为 new Lazy<foo>(() => new Foo())</foo> 中的 new Foo() 在 lazy 构造时就跑了,其实它被包进委托里,压根没执行。
- 构造函数参数是
Func<t></t>,不是T实例,所以不触发创建 - 如果传入的工厂方法抛异常,该异常会在首次访问
Value时抛出,且后续再访问仍会抛(Lazy<t></t>不重试) - 默认使用
LazyThreadSafetyMode.ExecutionAndPublication,无需额外加锁
如何控制线程安全模式?
用带 LazyThreadSafetyMode 参数的构造函数。默认是线程安全的,但如果你确定单线程环境,或想省掉内部锁开销,可显式设为 LazyThreadSafetyMode.None。
注意:设为 None 后,若多线程并发首次访问 Value,可能创建多个实例,且无法保证哪个实例最终被返回——这不是 bug,是设计如此。
-
LazyThreadSafetyMode.PublicationOnly:允许多次初始化,但只发布第一个成功的结果(适合初始化成本低、结果等价的场景) -
LazyThreadSafetyMode.ExecutionAndPublication(默认):严格保证只初始化一次、只发布一次 - 一旦传入
LazyThreadSafetyMode.None,就别在多线程里碰它的Value
为什么不能直接把对象赋给 Lazy?
因为 Lazy<t></t> 的核心价值就是“延迟 + 控制时机”,如果写成 new Lazy<foo>(new Foo())</foo>,编译根本过不去——构造函数只接受 Func<t></t>,不接受 T。
有人试图绕过:先 new 出来,再包进 lambda,比如 var f = new Foo(); new Lazy<foo>(() => f)</foo>。这虽然能编译,但完全失去懒加载意义——f 已在上一行创建了。
- 正确姿势永远是把 new 表达式或复杂初始化逻辑写在 lambda 里
- 如果初始化依赖外部状态(如配置、DB 连接),确保该状态在
Value首次访问时已就绪 - 避免在工厂委托里捕获大对象或长生命周期引用,以防内存泄漏
Lazy 和属性自动初始化(C# 6+)有什么区别?
像 private readonly Foo _foo = new Foo(); 或 public Foo Instance { get; } = new Foo();,都是类实例化时立即执行,和 Lazy<t></t> 的“按需”有本质区别。
典型误用场景:在 ASP.NET Core 的 Singleton service 里,用自动初始化字段加载耗时资源(如读配置文件、连远程服务),结果所有请求还没开始,应用启动就卡住——这时必须换 Lazy<t></t> 或 IServiceProvider 延迟解析。
- 自动初始化发生在构造函数执行期间,属于“ eager ”;
Lazy<t></t>是真正的 “lazy” -
Lazy<t></t>支持线程安全控制;字段初始化没有内置同步机制 - 如果初始化逻辑可能失败,
Lazy<t></t>的异常传播更可控(只影响首次访问者)
真正要注意的是:LazyValue 又持有了大资源或未释放句柄,记得考虑是否需要手动清理——Lazy<t></t> 本身不提供 Dispose 能力,得靠你自己在宿主类里做释放配合。


















