C#设计:单例模式

写在

单例模式在开发中经常会用到,单例模式是确保一个类只有一个实例,并且提供出一个供全局访问的点,例如开发中常用到的数据库操作实例,我们不可能每次操作数据库都要重新连接创建新的实例这样的效率太低了,通过单例模式可以只创建一次以后的每次访问都直接访问这个静态的单例即可。

在 ASP.NET Core 提供了DI容器,可以很方便的创建单例服务等,那么怎么不依赖DI容器实现一个简单的单例模式?

假设现在有一个类是 Drizzle,我们先用普通的方式去创建储存10个这样的实例:


class Drizzle
{
    
}

public static class Program
{
    public static void Main()
    {
        var list = new List<Drizzle>();
        for (int i = 0; i < 10; i++)
        {
            list.Add(new Drizzle());
        }

        foreach (var item in list)
        {
            Console.WriteLine(item.GetHashCode());
        }
    }
}
27252167
43942917
59941933
2606490
23458411
9799115
21083178
55530882
30015890
1707556

由结果可以看出,这是分别创建了十个不同的实例。

然后开始将Drizzle这个类改造成单例模式,这里主要说三种实现方式:

  • 饿汉模式
  • 内部静态类
  • 懒汉模式

另外在本文的最后还会讲一些其他的C#特定的语法实现单例

饿汉模式

首先来看饿汉模式,怎么去实现他呢?

  1. 创建私有的构造函数,这样不能通过外部new出新的实例
  2. 创建私有的静态实例,并且实例化
  3. 创建公有的获取实例的属性或者接口

按照上面的步骤我们可以把Drizzle类改成这样的:

class Drizzle
{
    // 静态私有实例
    private static readonly Drizzle _drizzle = new();

    // 构造函数私有化
    private Drizzle()
    {
    }

    // 获取实例的属性
    public static Drizzle Instance
    {
        get => _drizzle;
    }

    // 获取实例的方法
    public static Drizzle GetInstance() => Instance;
}

// Main中使用Drizzle.GetInstance()或者Drizzle.Instance替换new
// 例如
list.Add(Drizzle.GetInstance());
27252167
27252167
27252167
27252167
27252167
27252167
27252167
27252167
27252167
27252167

这样就实现了饿汉单例模式啦!

静态类模式

然后来看看静态类模式如何实现

  1. 创建私有的构造函数,这样不能通过外部new出新的实例
  2. 创建私有的内部静态类
  3. 在静态类中创建类的实例
  4. 创建公有的获取实例的属性或者接口

按照上面步骤我们可以将Drizzle改成:

class Drizzle
{
    // 创建私有静态类,并在内部实例化
    private static class CreateInstance
    {
        public static Drizzle Instance = new Drizzle();
    }

    // 获取实例的属性
    public static Drizzle Instance = CreateInstance.Instance;

    // 获取实例的方法
    public static Drizzle GetInstance() => Instance;
}

懒汉模式

最后一个,懒汉模式,它的步骤和上面两种方案也有很多共同点,但是需要加锁来解决线程安全,实现步骤如下:

  1. 创建私有的构造函数,这样不能通过外部new出新的实例
  2. 创建私有的静态实例,但是不实例化
  3. 建立静态只读的锁对象
  4. 创建公有的获取实例的属性或者接口

按照如上步骤,我们可以将Drizzle改造成:

class Drizzle
{
    // 创建私有静态类,暂不实例化
    private static Drizzle? _drizzle;

    // 创建锁
    private static readonly object _lock = new();

    // 获取实例的属性
    public static Drizzle Instance
    {
        get
        {
            if (_drizzle is null)
            {
                lock (_lock)
                {
                    _drizzle ??= new Drizzle();
                }
            }

            return _drizzle;
        }
    }

    // 获取实例的方法
    public static Drizzle GetInstance() => Instance;
}

这样一个线程安全的懒汉单例模式就完成啦,那么为什么要对懒汉模式做线程安全的操作呢?

首先我们对比饿汉和懒汉的区别,饿汉在创建静态实例的时候直接实例化,也就是程序运行的时候自动创建了这个唯一的单例,所以不会有线程上的不安全问题,而饿汉模式不会直接实例化,会在需要的时候进行实例化,但是如果在多线程的情况下,如果多个线程同时需要当前单例,但是单例还未实例化,这样会同时new出不同的实例,所以懒汉模式需要对获取实例的方法中加锁!

C#的 Lazy

public sealed class Singleton
{
    private static readonly Lazy<Singleton> _lazy = new Lazy<Singleton>(() => new Singleton());

    public static Singleton Instance => _lazy.Value;

    private Singleton() { }
}

这里使用了 Lazy<T> 类来实现懒加载,并在静态构造函数中初始化了一个只读的静态变量 lazy,该变量的值为一个 Lazy<Singleton> 对象,该对象用于确保 Singleton 类只被实例化一次。Instance 属性返回 lazy.Value,该属性在第一次调用时会初始化 Singleton 对象。由于构造函数是私有的,因此只能通过 Instance 属性访问该对象。

使用 sealed 关键字来防止其他类继承 Singleton 类。

Lazy<T> 类本身就是为了解决多线程下的线程安全问题而设计的,它的默认行为是懒加载并且线程安全。当多个线程同时尝试访问 lazy 变量时,只有一个线程能够实例化 Singleton 对象,而其他线程会等待该对象被实例化后再访问 Instance 属性,确保了线程安全性。

需要注意的是,如果要使用 .NET Core 中的 Lazy<T> 类来实现单例模式,需要在构造函数中传入一个 LazyThreadSafetyMode.ExecutionAndPublication 参数,以确保在多线程环境下实例化 Singleton 对象是线程安全的


评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注