在 C# 编程中,有时我们希望某个对象的初始化过程延迟到真正需要使用它的时候,这样可以提高程序的性能,减少不必要的资源占用。Lazy<T>
类型就为我们提供了这样的功能
一、Lazy<T> 是什么
Lazy<T> 是 C# 中用于延迟初始化的类型。其核心作用就是将对象的创建推迟到首次访问该对象的时候,而不是在声明变量时就进行创建。有以下特点:
- 延迟性:就像你有个任务,但你可以选择在真正要用到这个任务结果的时候才去执行它,而不是一开始就着手去做。
Lazy<T>
允许我们把对象的初始化操作推迟到首次访问Value
属性的时候 - 线程安全性:
Lazy<T>
提供了不同级别的线程安全模式,可以确保在多线程环境下对象的正确初始化和访问
Lazy<T> 自.NET Framework 4.0 开始引入。后续版本核心功能稳定,无重大功能或用法变化。只要项目基于.NET Framework 4.0 及以上版本,都能正常使用
二、推荐应用场景
Lazy<T>虽然强大,但并非所有场景都适用。以下这些场景尤其适合它发挥优势:
- 数据库连接:数据库连接的建立通常开销较大。使用
Lazy<T>
可以在真正需要执行数据库操作时才创建连接,避免程序启动时就占用过多资源 - 大型对象创建:比如创建一个庞大的配置对象,初始化过程复杂且可能在程序运行过程中不一定会用到,此时使用
Lazy<T>
可以延迟初始化,提升程序启动速度 - 资源加载:加载文件、图片等资源时,如果不是程序一开始就需要的,通过
Lazy<T>
可以在需要展示或使用这些资源时才进行加载 - 单例模式优化:实现单例模式时,用 Lazy<T> 可以做到线程安全的延迟初始化
三、实战 Demo
基础用法
下面的代码展示了 Lazy<T>
的基本使用,延迟初始化一个简单的字符串对象:
class Program{ static void Main() { // 声明一个 Lazy<string> 对象,推迟字符串的初始化 Lazy<string> lazyString = new Lazy<string>(() => "延迟初始化的字符串"); // 判断此时字符串是否被创建 Console.WriteLine("在访问 Value 之前,字符串是否已初始化: " + lazyString.IsValueCreated); // 访问 Value 属性,触发字符串的初始化 string value = lazyString.Value; Console.WriteLine("访问 Value 后,字符串是否已初始化: " + lazyString.IsValueCreated); Console.WriteLine("字符串的值: " + value); }}
class Program
{static void Main(){// 声明一个 Lazy<string> 对象,推迟字符串的初始化Lazy<string> lazyString = new Lazy<string>(() => "延迟初始化的字符串");// 判断此时字符串是否被创建Console.WriteLine("在访问 Value 之前,字符串是否已初始化: " + lazyString.IsValueCreated);// 访问 Value 属性,触发字符串的初始化string value = lazyString.Value;Console.WriteLine("访问 Value 后,字符串是否已初始化: " + lazyString.IsValueCreated);Console.WriteLine("字符串的值: " + value);}
}
- 首先创建了一个
Lazy<string>
对象 - 通过
IsValueCreated
属性判断对象是否已经初始化(false
表示对象还未初始化) - 当访问
lazyString.Value
时,会触发字符串的初始化,并将值赋给value
变量
进阶用法:单例模式的实现
传统单例模式实现要处理线程安全等复杂问题,用 Lazy<T> 可简洁实现线程安全的单例模式
单例类
class Singleton{ // 私有构造函数,防止外部直接创建实例 private Singleton(){} // 用 Lazy<T> 实现单例延迟初始化 private static Lazy<Singleton> lazyInstance = new Lazy<Singleton>(() => new Singleton()); // 静态属性获取单例实例 public static Singleton Instance { get { return lazyInstance.Value; } }}
class Singleton
{// 私有构造函数,防止外部直接创建实例private Singleton(){}// 用 Lazy<T> 实现单例延迟初始化private static Lazy<Singleton> lazyInstance = new Lazy<Singleton>(() => new Singleton());// 静态属性获取单例实例public static Singleton Instance{get { return lazyInstance.Value; }}
}
使用单例类
class Program{ static void Main() { // 获取单例实例,此时初始化 Singleton 对象 Singleton singleton1 = Singleton.Instance; // 再次获取单例实例,不会重新初始化 Singleton singleton2 = Singleton.Instance; Console.WriteLine($"单例实例 1 和 2 是否相同: {ReferenceEquals(singleton1, singleton2)}"); }}
class Program
{static void Main(){// 获取单例实例,此时初始化 Singleton 对象Singleton singleton1 = Singleton.Instance;// 再次获取单例实例,不会重新初始化Singleton singleton2 = Singleton.Instance;Console.WriteLine($"单例实例 1 和 2 是否相同: {ReferenceEquals(singleton1, singleton2)}");}
}
Lazy<Singleton>
确保Singleton
实例首次访问时创建,且因Lazy<T>
本身线程安全,无需额外线程同步代码,保证多线程环境下单例模式正确
四、核心方法和属性阐释
LazyThreadSafetyMode 枚举值
- LazyThreadSafetyMode.None:不提供线程安全保障,适用于单线程环境,性能最高
- LazyThreadSafetyMode.ExecutionAndPublication:线程安全,确保初始化仅执行一次,适用于多线程环境
- LazyThreadSafetyMode.PublicationOnly:线程安全,允许并发执行初始化,但仅使用第一个成功的结果,不太常用
构造函数
1. 无参数构造函数:默认使用LazyThreadSafetyMode.None
2. 带初始化委托的构造函数:默认使用LazyThreadSafetyMode.ExecutionAndPublication
(线程安全)。确保初始化委托仅执行一次,多线程会等待初始化完成后共享同一实例
3. 显式指定线程安全模式的构造函数:
isThreadSafe
为true
时等价于LazyThreadSafetyMode.ExecutionAndPublication
isThreadSafe 为 false
等价于LazyThreadSafetyMode.None
4. 带初始化委托和线程安全模式的构造函数:
最灵活的构造函数,同时控制初始化逻辑和线程安全
- 适用需要自定义初始化且需精确控制线程安全的场景
5. 构造函数Demo
// 1. 无参数(默认非线程安全)
var lazy1 = new Lazy<MyClass>();
// 2. 带初始化委托(默认线程安全)
var lazy2 = new Lazy<MyClass>(() => new MyClass("custom"));
// 3. 显式指定线程安全模式
var lazy3 = new Lazy<MyClass>(LazyThreadSafetyMode.PublicationOnly);
// 4. 自定义初始化 + 显式模式
var lazy4 = new Lazy<MyClass>(() => new MyClass(), LazyThreadSafetyMode.None);
常用属性
1. Value:获取延迟初始化对象值的关键属性。在首次访问时触发对象初始化,后续访问直接返回已初始化的值
2. IsValueCreated:判断对象是否已初始化。在需要根据对象是否初始化来执行不同逻辑时非常有用,在某些情况下避免重复初始化操作
五、避坑指南、注意事项
1. 委托异常处理:构造函数委托执行抛异常,Lazy<T> 不再尝试初始化,后续访问 Value
仍抛异常,委托里要处理好异常
2. 线程安全模式选择:多线程环境要根据实际需求选择合适的线程安全模式。单线程环境使用None 可以获得最佳性能;多线程环境,通常选择ExecutionAndPublication 以确保初始化的线程安全性
3. 内存泄漏风险:延迟初始化对象若持非托管资源,没正确释放会内存泄漏,注意用完要释放资源
4. 性能考量:虽然延迟初始化可以提升性能,但初始化开销小的对象,用 Lazy<T> 可能会增加额外的复杂性和性能开销,需权衡是否真的需要延迟初始化
5. 调试注意:因延迟初始化,调试难跟踪,可在委托设断点观察
六、总结与决策
- Lazy<T> 是 C# 优化性能和管理资源的好帮手,它的价值在于实现对象的延迟初始化,有效提升程序性能,减少资源浪费,尤其在多线程环境和处理开销较大的对象初始化时表现出色
- 了解其基本概念和用法,能帮助理解延迟初始化的思想。掌握其在多线程环境下的使用、线程安全模式选择以及异常处理等进阶知识,可以在实际项目中更好地优化性能