用心理解设计模式——单例模式 (Singleton Pattern)

前置文章: 设计模式的原则 

其他设计模式:用心理解设计模式专栏

设计模式相关代码已统一放至 我的 Github

一、定义

  创建型模式之一。

  Ensure a class has only one instance, and provide a global point of access to it.

(确保某一个类只有一个实例,而且自行实例化并吸纳过整个系统提供这个实例)

二、要点

  将类的非静态构造函数私有化。只在类内部实例一次。

三、评价

  只允许创建单个实例,满足一些管理需求,节省内存,加快对象访问速度等。

四、实现方式

  1.饿汉式

    顾名思义:比较饥渴、比较急,不管用不用,类加载时就先创建实例。

public sealed class Singleton 
{
    static private readonly Singleton instance = new Singleton();

    static public Singleton GetInstance()
    {
        return instance;
    }
	
    private Singleton() { }
}

  2.懒汉式

    顾名思义:比较懒,等到用的时候才创建实例。

public sealed class Singleton 
{
    static private Singleton instance;

    static public Singleton GetInstance()
    {
        if (instance == null) { instance = new Singleton(); }
        return instance;
    }
	
    private Singleton() { }
}

两种基本方式比较:

  线程安全?:

    饿汉式,是线程安全的,因为,类只会加载一次,实例必然只会创建一次。

    懒汉式,是线程不安全的,因为,多线程下,如果不同线程同时进入 if判断,就会生成多个实例对象。

  延迟实例化?:

    饿汉式,不管是否使用都会创建实例,如果最终没有使用,造成内存浪费。

    懒汉式,实际使用时才创建实例,节省内存。

3.静态初始化式 (饿汉模式在Lazy Loading上的优化(不彻底)。.Net中首选的方式)

public sealed class Singleton 
{
    static private readonly Singleton instance;

    static public Singleton GetInstance()
    {
        return instance;
    }
    
    //静态构造
    static Singleton()
    {
        instance = new Singleton();
    }
	
    private Singleton() { }
}

  要点理解: C#静态构造函数

  特点:它是线程安全的, 并且将实例化从类加载时延迟到了类首次使用时。

4.双重检查锁模式 (饱汉模式在多线程上的优化)

public sealed class Singleton
{
    private static Singleton instance;

    private static readonly object theLock = new object ();
 
    private Singleton () { }
 
    public static Singleton GetInstance ()
    {
        // 第一重检查, 避免 “实例已经存在时仍然执行lock获取锁, 影响性能” 的问题。
        if (instance == null) 
        {
            //让线程排队执行被lock包围的代码段
            lock (theLock) 
            {
                //第二重检查, 仅让队首的线程进入if创建实例。
                if (instance == null) 
                {
                    instance = new Singleton ();
                }
            }
        }
        return instance;
    }
}

   要点理解: C# 的 lock 关键字  。

   特点:它是线程安全的,并且将实例化延迟到了单例使用时。

5.静态内部类式

public sealed class Singleton
{
    //此静态内部类,只有在第一次使用时才会加载
    static private class Holder
    {
        static public readonly Singleton instance = new Singleton();
    }

    static public Singleton GetInstance()
    {
        return Holder.instance;
    }

    private Singleton() { }
}

  要点理解:内部类在第一次使用时才被加载,其直接在定义时初始化的静态成员也在此时进行初始化。
  特点:它是线程安全的,并且将实例化延迟到了单例使用时。

6.Lazy<T>式

//注意:仅.NET4 以上可用
public sealed class Singleton
{
    static private readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
 
    public static Singleton GetInstance 
    {
        return lazy.Value; 
    }
 
    private Singleton() { }
}

  要点理解:如何执行对象的延迟初始化

  特点:它是线程安全的,并且将实例化延迟到了单例使用时。适合较大单例对象。

五、通用的单例 

  在实际项目中,常出现大量单例类,如果每个都要实现一遍单例过程,会比较麻烦。

  想要实现通用的单例,要满足两个条件:

    1、构造函数设为私有(必须!这是单例的基本要素)。达到单例的目的。

    2、提供一个公共的类(公共的父类或工厂类),在这个类中统一构造不同的单例类。达到通用的目的。

  但是显然,这两个条件是互相矛盾的! 构造函数为私有的类,不能在其他类中进行构造。

 (目前有很多写法都放弃了第1条要素,这对单例来说是极其不安全的。)

  那怎么办呢?幸好有反射,“利用反射,可以在使用私有构造函数,在类的外部进行构造”, 这是单例通用的关键。

  ( 这里以 静态初始化式来展示,其他方式类似)。

using System;
using System.Reflection;
using UnityEngine;

namespace Singleton.Generic
{
    //泛型单例抽象类
    public abstract class Singleton<T> where T : Singleton<T>
    {
        static private T instance;

        static public T GetInstance()
        {
            return instance;
        }

        //静态构造
        static Singleton()
        {
            //instance = new T();   //不可行。需要“公有的无参构造函数”
            //instance = (T)Activator.CreateInstance(typeof(T));  //不可行,这种反射构造方式需要“公有的构造函数”
            //instance = (T)Assembly.GetAssembly(typeof(T)).CreateInstance(typeof(T).ToString());//不可行。这种反射构造方式需要“公有的构造函数”

            //可以利用反射在类外部调用类的私有无参构造函数进行构造!!!
            var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
            var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0);
            if (ctor == null)
            {
                throw new Exception("\"" + typeof(T).ToString() + "\"类中不存在私有无参构造函数");
            }
            instance = (T)ctor.Invoke(null);
        }

        //为了能被子类继承,父类的构造必须不能私有
        //private Singleton() { }
        protected Singleton() { }
    }

    //通过继承实现单例类
    sealed public class A : Singleton<A>
    {
        private A() { }
    }

    //通过组合实现单例类(可以继承其他类)
    sealed public class B 
    {
        private B() { }

        static public B GetInstance()
        {
            //Singleton<T>类不仅可以当做单例基类,还可作为一个单例工厂(传入类名,传出单例)
            return Singleton<B>.GetInstance();
        }
    }

    //直接在客户中,使用单例工厂获取单例
    sealed public class C
    {
        private C() { }
    }

    public class Client
    {
        static public void Main()
        {
            //方式一、继承的单例
            A a = A.GetInstance();
            //方式二、内部调用静态泛型工厂
            B b = B.GetInstance();
            //方式三、直接使用静态泛型工厂
            C c = Singleton<C>.GetInstance();

            //A a2 = new A();   //无法直接构造, 达成单例目的
            //B b2 = new B();   //无法直接构造, 达成单例目的
            //C c2 = new C();   //无法直接构造, 达成单例目的

            //直观说明"继承"和"静态工厂"这两种方式最终都是一个单例。
            Debug.Log(A.GetInstance() == Singleton<A>.GetInstance()); //返回true
        }
    }
}
//Lazy<T> 式
using System;
using System.Reflection;

public abstract class Singleton<T> where T : Singleton<T>
{
    static private readonly Lazy<T> lazy = new Lazy<T>(() => 
    {
        var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
        var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0);
        if (ctor == null) { throw new Exception("\"" + typeof(T).ToString() + "\"类中不存在私有无参构造函数"); }
        return ctor.Invoke(null) as T;
    });

    static public T Instance
    {
        get { return lazy.Value; }
    }

    protected Singleton() { }
}

  注意: Singleton<T>类在实际使用时,可以让它作为一个单例基类,也可作为一个静态的单例工厂(传入类名,传出单例)。作为单例工厂时,可以解决 “继承自其他类的类,又需要是单例” 的问题。

  实际上,因为GetInstance()是一个静态方法(静态方法的继承,本质上只是由子类原封不动地调用父类的该静态方法)。所以Singleton<T>类在作为单例基类时,子类并没用从它身上继承到任何东西,所以,这个基类就是被当做一个静态类在用。两种用法本质上是一样的,都是一个静态单例工厂。

六、静态类和单例模式的比较

这两者,让人常常很纠结到底应该选哪个。

从以下几个方面作对比:

1.最直观的,看类是否有成员变量,如果一个类中只是提供一些方法,没有成员变量。那实在没必要使用单例类,直接用静态类即可。

2.从执行效率,静态类执行效率更高。

3.从成员大小,如果类的职责比较庞大,并且只是在需要的时候才可能用到。就应该考虑是否需要懒加载优化内存,静态类最多推迟到在静态构造方法中初始化静态成员变量,而单例可以推迟到实际使用时。

4.从编程思想,单例类面向对象,可以继承类(注意不能被继承,上边说了),可以有动态多态,可以实现接口。静态类面向过程

5.从使用简便性,静态类直接 类名.方法() ;单例模式要用 类名.GetInstance().方法()。

七、单例常驻内存问题

由于单例模式中总是静态引用一个类的实例,导致该对象始终不会被自动回收,那么,应该根据实际情况,考虑是否主动销毁单例对象。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NRatel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值