在C#开发过程中,很多场景下我们不需要在对象创建时就立即完成所有初始化工作,比如某些大对象、数据库连接或者耗时的配置加载逻辑,提前初始化会浪费系统资源,甚至影响程序启动速度。延迟初始化就是解决这类问题的常用方案,而.NET提供的Lazy类则是实现延迟初始化的标准工具,同时天然支持线程安全,避免了手动实现时的多线程问题。

Lazy基础用法
Lazy的核心逻辑是包裹一个需要延迟初始化的对象,只有当第一次访问Value属性时,才会触发初始化逻辑。最简单的使用方式是不传递任何参数,此时Lazy会通过被包裹类型的无参构造函数完成初始化。
using System;
namespace LazyDemo
{
class BigObject
{
public BigObject()
{
Console.WriteLine("BigObject初始化完成,耗时操作执行");
}
public void DoWork()
{
Console.WriteLine("BigObject执行工作逻辑");
}
}
class Program
{
static void Main(string[] args)
{
// 创建Lazy实例,包裹BigObject类型
Lazy<BigObject> lazyObj = new Lazy<BigObject>();
Console.WriteLine("Lazy实例创建完成,尚未初始化BigObject");
// 第一次访问Value属性,触发初始化
lazyObj.Value.DoWork();
Console.WriteLine("第一次访问Value后,再次访问Value:");
// 第二次访问不会再次初始化
lazyObj.Value.DoWork();
}
}
}
运行上述代码可以看到,输出顺序为首先打印Lazy实例创建完成的提示,然后才会执行BigObject的构造函数,说明初始化确实是在访问Value时才触发的,且第二次访问Value时不会重复执行初始化逻辑。
自定义初始化逻辑
如果被包裹的类型没有无参构造函数,或者我们需要自定义初始化逻辑,可以在Lazy的构造函数中传入一个委托,委托中定义初始化的具体过程。
using System;
namespace LazyDemo
{
class ConfigLoader
{
public string ConfigContent { get; private set; }
// 带参数的构造函数,模拟加载配置文件的耗时操作
public ConfigLoader(string filePath)
{
Console.WriteLine($"开始从{filePath}加载配置");
// 模拟耗时操作
System.Threading.Thread.Sleep(1000);
ConfigContent = "app_name=test;max_count=100";
Console.WriteLine("配置加载完成");
}
}
class Program
{
static void Main(string[] args)
{
// 传入自定义初始化委托,指定配置路径
Lazy<ConfigLoader> lazyConfig = new Lazy<ConfigLoader>(() => new ConfigLoader("app.config"));
Console.WriteLine("Lazy配置实例创建完成");
// 第一次访问Value触发配置加载
Console.WriteLine($"配置内容:{lazyConfig.Value.ConfigContent}");
}
}
}
Lazy的线程安全机制
在多线程环境下,如果多个线程同时首次访问Lazy的Value属性,需要确保初始化逻辑只执行一次,Lazy默认就提供了线程安全保障。Lazy的构造函数可以传入LazyThreadSafetyMode枚举来指定线程安全模式,常用的模式有三种:
- None:不保证线程安全,多个线程同时访问可能触发多次初始化,仅适用于单线程场景
- PublicationOnly:多个线程同时触发初始化时,会执行多次初始化逻辑,但只有第一个完成的结果会被保留作为Value的值
- ExecutionAndPublication:默认的线程安全模式,使用锁机制保证初始化逻辑只执行一次,即使多个线程同时访问,也只有一个线程执行初始化,其他线程会等待初始化完成
下面通过多线程示例验证默认模式的线程安全特性:
using System;
using System.Threading;
namespace LazyDemo
{
class ThreadSafeObj
{
public ThreadSafeObj()
{
Console.WriteLine($"ThreadSafeObj初始化,线程ID:{Thread.CurrentThread.ManagedThreadId}");
}
}
class Program
{
static void Main(string[] args)
{
Lazy<ThreadSafeObj> lazyThreadObj = new Lazy<ThreadSafeObj>();
// 创建5个线程同时访问Value
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++)
{
threads[i] = new Thread(() =>
{
var obj = lazyThreadObj.Value;
});
}
// 启动所有线程
foreach (var thread in threads)
{
thread.Start();
}
// 等待所有线程执行完成
foreach (var thread in threads)
{
thread.Join();
}
}
}
}
运行代码后可以看到,ThreadSafeObj的构造函数只会执行一次,即使5个线程同时访问Value,也只有一个线程完成了初始化,其他线程等待后直接获取已初始化的对象,证明了默认模式的线程安全特性。
手动实现延迟初始化的问题
在没有Lazy类之前,开发者可能需要手动实现延迟初始化,常见的写法如下:
using System;
namespace LazyDemo
{
class ManualLazyObj
{
private ManualLazyObj _instance;
private readonly object _lock = new object();
public ManualLazyObj Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ManualLazyObj();
}
}
}
return _instance;
}
}
private ManualLazyObj()
{
Console.WriteLine("手动实现延迟初始化的对象创建");
}
}
}
这种双重检查锁的实现方式不仅代码冗长,还容易出现锁使用不当、逻辑遗漏等问题,而Lazy类已经内置了这些逻辑,使用起来更加简洁且不易出错。
Lazy的常见使用场景
Lazy适合用在以下场景:
- 初始化成本高的对象,比如大对象、需要加载大量数据的对象
- 程序启动时需要快速启动,不需要立即使用的组件
- 单例模式的实现,结合Lazy可以很方便地实现线程安全的单例
- 依赖注入场景中,某些依赖不需要立即初始化,可以延迟到使用时再创建
需要注意的是,Lazy的Value属性一旦初始化完成就不可更改,且Lazy实例本身不建议作为可变的公共字段使用,最好作为只读的私有字段或者属性暴露,避免外部修改导致意外问题。