在 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:线程安全,允许并发执行初始化,但仅使用第一个成功的结果,不太常用

构造函数

一文读懂 C# 中的 Lazy<T>_字符串

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# 优化性能和管理资源的好帮手,它的价值在于实现对象的延迟初始化,有效提升程序性能,减少资源浪费,尤其在多线程环境和处理开销较大的对象初始化时表现出色
  • 了解其基本概念和用法,能帮助理解延迟初始化的思想。掌握其在多线程环境下的使用、线程安全模式选择以及异常处理等进阶知识,可以在实际项目中更好地优化性能