最近在总结以前做过的项目中用到的技术,发现用过不少的单例模板。而这些单例模板针对使用的场景不同,还存在着一些区别。所以重新温习了一下这方面的知识,做一下总结以便以后也能更快的回忆起来。

单例模式的作用

  • 保证一个类只有一个实例,并提供一个访问它的全局访问点。

单例的适用之处

  • 当确保一个类仅有一个实例,并且需要提供一个全局访问点时
  • 如果某个类需要频繁的创建和销毁,并且此过程开销比较大时
  • 如果需要对某些资源进行统一的管理或共享时

C#中

c#中基本的单例使用

让类自身负责保存它的唯一实例,并保证该类没有其它实例可构建,然后提供一个可供访问唯一实例的全局访问方法。

Class Singleton
{
    private static Singleton instance;    //私有静态成员,存储单例实例
    
    private Singleton {}    //私有构造,不能在外部被创建

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

}
  1. 这里将GetInstance()设置为静态类型的原因在于,静态类型会被定义成静态成员。而静态成员属于类本身而不是类的实例,在内存中只有一份,通过类名可以直接访问。因为不能在外部创建实例,进而只能用此种方式调用。静态方法内只能使用静态类型成员,instance也必须为静态
  2. 因为静态成员存储在静态存储区,new创建的唯一类实例存储在堆区,因此在程序结束时,才会被销毁。而new返回的实例的引用存储在栈区,方法结束就会被销毁
  3. 这里的静态方法也可以进行替换,使用字段来实现也可以
...
public static Singleton Instance 
{
    get
    { 
        if (instance is null)
        {
            instance = new Singleton();
        }
        return instance;
    }
    private set { instance = value; }
        
}
...

多线程下的C#单例

基础使用中的单例适用用单线程的环境,下面总结下多线程环境下的。在多线程中,对于临界资源的访问一定要高度重视,可能涉及到使用临界区以及同步机制。

饿汉式单例类

在自己被加载时就将自己实例化

public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton GetInstance()
    {
        return instance;
    }
}
  1. 使用sealed防止发生派生,派生可能会增加实例
  2. readonly,表明只能在静态初始化或者构造函数中分配变量
  3. 在多线程下,CLR会确保只有一个线程进行对象初始化工作
  4. 仅保证提供线程安全的实例创建和访问,对于线程中的数据需要进一步保证安全性
  5. 因为执行顺序原因,可能会造成空引用

懒汉式单例类

在自己被第一次引用的时候,才会将自己实例化

基本的单例使用中的单例就属于懒汉式单例

class Singleton
{
    private static volatile Singleton instance;
    private static readonly object syncRoot = new object();
    private Singleton() {}
    public static GetInstance()
    {
        if (instance is null)
        {
            lock(syncRoot)
            {
                if (instance is null)
                {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}
  1.  普通的懒汉式单例,存在线程安全问题,通过使用双重锁定来避免。第一次判断空用来判断是否已经创建过实例;加锁保证只有一个线程可以访问实例化语句;第二次判断空用来避免多个线程同时等待lock,lock结束后,导致有其它线程重复访问实例化语句,因此需要再次判空
  2. 延迟加载,在调用时才创建,避免不必要资源消耗
  3. volatile 确保每次读取变量都是内存中最新的值,确保多线程下对变量操作按顺序执行

单例模板

 懒汉式

 非多线程环境下的,去掉双重锁定就行

public class Singleton<T> where T : Singleton<T>, new()
{
    private static volatile T instance;
    private static readonly object syncRoot = new object();
    public static T GetInstance()
    {
        if (instance is null)
        {
            lock(syncRoot)
            {
                if (instance is null)
                {
                    instance = new T();
                }
            }
        }
        return instance;
    }    

}

饿汉式

public class Singleton<T> where T : Singleton<T>, new()
{
    private static T instance = new T();
    public static T GetInstance()
    {
        return instance;
    }
}

Unity中

继承MonoBehaviour的单例类,不用去考虑构造函数的问题。

unity中基本的单例使用

public class Singleton : MonoBehaviour
{
    
    private static Singleton instance;
    public static Singleton Instance
    {
        get
        {
            return instance;
        }
    }

...

    public void Awake()
    {
        instance = this;
    }

}

由于没有构造函数,在Awake周期函数中定义instance存储对象为该组件本身,因为将脚本挂载在物体上后,创建了对象。

单例模板

代码自动挂载

需要挂载在物体上的,这里只写了懒汉式

public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
    private static volatile T instance;
    private static readonly object syncRoot = new object();
    public static T GetInstance()
    {
        if (instance is null)
        {
            lock(syncRoot)
            {
                if (instance is null)
                {
                    instance = FindObjectOfType<T>();

                    if (instance is null)
                    {
                        GameObject singletonObject = new GameObject();
                        instance = singletonObject.AddComponent<T>();
                        singletonObject.name = typeof(T).ToString() + " (Singleton)";
                        DontDestroyOnLoad(singletonObject);
                    }
                }
            }
        }
        return instance;
    }    

}

先查找在场景中是否有已经存在的实例,否则就创建个新的物体,并添加组件。实例化的过程是通过AddComponent实现的。

手动挂载

public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
    private static T instance;

    protected virtual void Awake()
    {
        if(instance == null)
        {
            instance = (T)this;
            DontDestroyOnLoad(gameObject);
        }
        else if (instance != this)
        {
            Destroy(gameobject);
        }
    }

    public static T GetInstance()
    {
        return instance;
    }

}

这种在单线程下执行,并保证脚本挂载的物体在场景切换时不摧毁,在Awake中进行初始化。如果不写DontDestroyOnLoad,在切换场景时,会先删除物体,切换回后,重新创建物体,会重新执行Awake,导致不符合唯一性

不继承MonoBehaviour,使用周期函数

思路是通过在一个继承MonoBehaviour的脚本中添加几种委托,将几种放在对应周期函数中执行,然后给出向不同委托添加新成员的公有方法。未继承MonoBehaviour的脚本通过调用这些公有方法,并把自己需要在对应周期中执行的函数作为参数传递进方法中,就可以实现不继承MonoBehaviour而使用周期函数。

public class MyMono : MonoBehaviour
{
    private event UnityAction updateEvent;
    private event UnityAction fixedUpdateEvent;

    private void Start()
    {
        DontDestroyOnLoad(gameObject);
    }

    private void Update()
    {
        if (updateEvent != null)
        {
            updateEvent();
        }
    }    

    private void FixedUpdate()
    {
        if (fixedUpdateEvent != null)
        {
            fixedUpdateEvent();
        }
    }
    
    public void AddUpdateListener(UnityAction func)
    {
        updateEvent += func;
    }

    public void RemoveUpdateListener(UnityAction func)
    {
        updateEvent -= func;
    }
    
    public void AddFixedUpdateListener(UnityAction func)
    {
        fixedUpdateEvent += func;
    }

    public void AddFixedUpdateListener(UnityAction func)
    {
        fixedUpdateEvent -= func;
    }
}

这里不使用UnityAction也可以,用Action、Func 、UnityEvent等是一样的效果。注册进去的方法,通过多播委托统一调用。

添加event约束,不能在外部调用,不能直接赋值,不能作为临时变量

  • UnityAction是对Action的一层包装,但是最多只能接受4个参数,无返回值
  • UnityEvent,最多接受4个参数,泛型版本是抽象类需要继承实现,注册移除触发有特定的方法,分为非持久性和持久性,持久性可在inspector上点击添加或删除,无返回值
  • Action无返回值,支持16个参数
  • Func有返回值,支持最多17个参数,最后一个参数表示返回值类型
public calss MonoSingleton : Singleton<MonoSingleton>
{
    private MyMono myMono;
    
    public MonoSingleton()
    {
        GameObject obj = new GameObject();
        myMono = obj.AddComponent<MonoController>();        
    }

    public void AddUpdateListener(UnityAction func)
    {
        myMono.AddUpdateListener(func);
    }

    public void RemoveUpdateListener(UnityAction func)
    {
        myMono.RemoveUpdateListener(func);
    }
    
    public void AddFixUpdateListener(UnityAction func)
    {
        myMono.AddFixedUpdateListener(func);
    }

    public void RemoveFixUpdateListener(UnityAction func)
    {
        myMono.RemoveFiUpdateListener(func);
    }

    public Coroutine StartCoroutine(IEnumerator routine)
    {
        return myMono.StartCoroutine(routine);
    }

    public Coroutine StartCoroutine(string methodName)
    {
        return myMono.StartCoroutine(methodName);
    }

    public Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value)
    {
         return myMono.StartCoroutine(methodName, value);
    }

}

这里选择c#懒汉单例模板 作为基类

创建实例时的不同方法

除了常规的通过new来创建某个类型的对象外,还可以用反射动态创建指定类型的实例

不需要在编译时确定具体的类型,第二个参数true表示允许调用私有构造函数。

需要一定的性能开销

T instance = Activator.CreateInstance(typeof(T), true) as T;
  1. 可以在Awake中得到自身的this引用进行初始化
  2. 通过AddComponent<T>();进行初始化
  3. 通过FindObjectOfType<T>();进行初始化
  4. 通过反射动态创建对象
  1. 不继承MonoBehaviour的单例类
  1. 通过new来进行初始化
  2. 通过反射进行初始化

存在的问题

  • 在多线程环境下需要处理并发访问的问题
  • GC不能去控制释放,在程序结束之后才能销毁。有时需要手动控制释放
  • 不符合单一职责原则,一个类负责很多功能
  • 全局可以访问,可能会增加代码的耦合度
  • 隐藏了构造函数,在依赖注入时比较麻烦
  • 隐藏了类之间的依赖关系,在结构方面可能会不清晰
  • 如果采用类加载进行初始化的方式,由于执行顺序原因,可能会导致空引用

最后对于唯一性方面的 一些补充