Lazy写单例的正确写法

今天修改代码的时候发现同事在单例的私有构造器写了任务,这样会不会导致每次使用单例都会开一个Task,所以我好奇单例的私有构造究竟执行多少次

我对单例的写法有了好奇心,写了4种,朋友们可以看看哪个是正确的写法,

 public class PlcHelper
 {
     static PlcHelper()
     {
     }

     private static Lazy<PlcHelper> _instance = new Lazy<PlcHelper>(()=>new PlcHelper());

     public static PlcHelper GetPlcHelper = _instance.Value;
 }

 public class PlcHelper2
 {
     static PlcHelper2()
     {
     }

     private static Lazy<PlcHelper2> _instance => new Lazy<PlcHelper2>(() => new PlcHelper2());

     public static PlcHelper2 GetPlcHelper = _instance.Value;
 }

 public class PlcHelper3
 {
     static PlcHelper3()
     {
     }

     private static Lazy<PlcHelper3> _instance = new Lazy<PlcHelper3>(() => new PlcHelper3());

     public static PlcHelper3 GetPlcHelper => _instance.Value;
 }

 public class PlcHelper4
 {
     static PlcHelper4()
     {
     }

     private static Lazy<PlcHelper4> _instance => new Lazy<PlcHelper4>(() => new PlcHelper4());

     public static PlcHelper4 GetPlcHelper => _instance.Value;
 }

详细对比表格

特性 PlcHelper PlcHelper2 PlcHelper3 PlcHelper4
_instance 声明 = 字段赋值 => 只读属性 = 字段赋值 => 只读属性
_instance 初始化时机 类加载时一次 每次访问重新创建 类加载时一次 每次访问重新创建
GetPlcHelper 声明 = 公共字段 = 公共字段 => 只读属性 => 只读属性
GetPlcHelper 返回值时机 类加载时固定 类加载时固定 每次访问时获取 每次访问时获取
是否线程安全 ✅ 是 ❌ 否 ✅ 是 ❌ 否
是否真正单例 ✅ 是 ❌ 否 ✅ 是 ❌ 否
内存效率 ✅ 好 ❌ 差(重复创建Lazy) ✅ 好 ❌ 差(重复创建Lazy)
延迟加载 ✅ 是 ⚠️ 每次返回不同Lazy ✅ 是 ⚠️ 每次返回不同Lazy
推荐使用 ⚠️ 可以但不规范 ❌ 错误 ✅ 最佳实践 ❌ 错误

核心区别图解

PlcHelper(字段+字段)⚠️

text

类加载时: _instance = [Lazy对象] (创建一次)
类加载时: GetPlcHelper = _instance.Value (固定为实例引用)
使用时: GetPlcHelper → 直接返回固定实例 ✅

PlcHelper2(属性+字段)❌

text

每次访问 _instance: 创建新 [Lazy对象] 
类加载时: GetPlcHelper = _instance.Value (当时那次的Value)
使用时: GetPlcHelper → 返回第一次创建的实例 ⚠️
但浪费了多个Lazy对象!

PlcHelper3(字段+属性)✅

text

类加载时: _instance = [Lazy对象] (创建一次)
使用时: GetPlcHelper → 每次获取 _instance.Value → 同一个实例 ✅

PlcHelper4(属性+属性)❌

text

每次访问 _instance: 创建新 [Lazy对象]
每次访问 GetPlcHelper: 重新调用 _instance.Value → 每次都创建新Lazy
后果: 每次获取"单例"可能不同!❌

详细代码分析

✅ PlcHelper3 - 唯一完全正确的写法

csharp

private static Lazy<PlcHelper3> _instance = new Lazy<PlcHelper3>(() => new PlcHelper3());
public static PlcHelper3 GetPlcHelper => _instance.Value;

执行流程:

  1. 类加载时:创建一个 Lazy<PlcHelper3> 对象
  2. 首次调用 GetPlcHelper:_instance.Value 触发,创建一个 PlcHelper3 实例
  3. 后续调用:直接返回同一个实例

优点:

  • ✅ 真正的懒加载(首次使用时创建)
  • ✅ 线程安全(Lazy默认线程安全)
  • ✅ 只读属性,封装良好
  • ✅ 内存效率高

⚠️ PlcHelper - 功能正确但不规范

csharp

private static Lazy<PlcHelper> _instance = new Lazy<PlcHelper>(()=>new PlcHelper());
public static PlcHelper GetPlcHelper = _instance.Value;  // 公共字段

问题:

  • 暴露公共字段而非属性(违反封装原则)
  • GetPlcHelper 在类加载时就固定了值,失去了Lazy的意义(虽然Lazy内部还没创建实例)
  • 无法添加后续逻辑(如日志、验证等)

实际上:

csharp

// 这种写法等价于:
private static PlcHelper _instance = new PlcHelper();  // 直接饿汉式

因为 _instance.Value 在静态初始化时就执行了。


❌ PlcHelper2 - 错误写法

csharp

private static Lazy<PlcHelper2> _instance => new Lazy<PlcHelper2>(() => new PlcHelper2());
public static PlcHelper2 GetPlcHelper = _instance.Value;

问题分析:

csharp

// _instance 是只读属性,每次访问都执行右侧代码
// 等价于:
private static Lazy<PlcHelper2> _instance 
{ 
    get { return new Lazy<PlcHelper2>(() => new PlcHelper2()); }
}

// GetPlcHelper 在静态构造函数中执行
// 此时 _instance 被调用,创建第1个Lazy对象,获取其Value
// 后续 GetPlcHelper 返回的是第1个Lazy的Value

// 但!如果其他代码访问 _instance(比如反射),会不断创建新Lazy对象

后果:

  • 浪费内存(可能创建多个Lazy对象)
  • 线程安全性不确定(多个Lazy对象)
  • 不符合直觉

❌ PlcHelper4 - 最严重错误

csharp

private static Lazy<PlcHelper4> _instance => new Lazy<PlcHelper4>(() => new PlcHelper4());
public static PlcHelper4 GetPlcHelper => _instance.Value;

致命问题:

csharp

// 每次调用 GetPlcHelper 都会:
// 1. 访问 _instance 属性
// 2. 创建新的 Lazy<PlcHelper4> 对象
// 3. 调用这个新Lazy的Value
// 4. 创建新的 PlcHelper4 实例

var obj1 = PlcHelper4.GetPlcHelper;  // 创建 Lazy#1 → 创建实例#1
var obj2 = PlcHelper4.GetPlcHelper;  // 创建 Lazy#2 → 创建实例#2
// obj1 != obj2 ❌ 不是单例!

这不是单例!这是每次调用都创建新对象!


验证代码

csharp

// 测试是否是单例
Console.WriteLine($"PlcHelper: {ReferenceEquals(PlcHelper.GetPlcHelper, PlcHelper.GetPlcHelper)}");
// 输出: True ✅

Console.WriteLine($"PlcHelper2: {ReferenceEquals(PlcHelper2.GetPlcHelper, PlcHelper2.GetPlcHelper)}");
// 输出: True ✅ (巧合,因为字段固定了第一次的Value)

Console.WriteLine($"PlcHelper3: {ReferenceEquals(PlcHelper3.GetPlcHelper, PlcHelper3.GetPlcHelper)}");
// 输出: True ✅

Console.WriteLine($"PlcHelper4: {ReferenceEquals(PlcHelper4.GetPlcHelper, PlcHelper4.GetPlcHelper)}");
// 输出: False ❌ (每次创建新对象!)

最终推荐

排名 写法 推荐度 理由
1 PlcHelper3 ⭐⭐⭐⭐⭐ 唯一完全正确且规范的写法
2 PlcHelper ⭐⭐ 功能正确但封装差,建议改用属性
3 PlcHelper2 ❌ 功能勉强正确但浪费内存,不推荐
4 PlcHelper4 ❌❌❌ 严重错误!不是单例,绝对不要用

标准单例写法(推荐模板)

csharp

public sealed class PlcHelper  // sealed 推荐
{
    // 私有构造函数
    private PlcHelper() { }
    
    // 使用 Lazy<T> 保证线程安全和懒加载
    private static readonly Lazy<PlcHelper> _instance = 
        new Lazy<PlcHelper>(() => new PlcHelper());
    
    // 公开的属性(不是字段)
    public static PlcHelper Instance => _instance.Value;
}

// 使用
var plc = PlcHelper.Instance;
posted @ 2026-05-12 15:49  孤沉  阅读(25)  评论(0)    收藏  举报