Unity的三种单例模式(饿汉,懒汉,MonoBehaviour)

Unity下三种单例模式

单例的使用场景

​ 对象全局唯一,且经常被使用。

static静态字段介绍

  1. 所有对象共享static属性,即static属性在此类中是唯一的。
  2. static属性不会被GC回收,随着程序开始而创建,随着程序结束而销毁(so 不要滥用单例哦)

学过面向对象的小伙伴对static想必都不陌生,其具有的特质和今天要讲解的单例十分相似,自然后续的单例模式也会使用到。

刨根问底:static属性为何类中唯一共享?

​ C#中创建的所有类都会存在一个全局唯一的类型对象(System.Type),类型对象中会保存此类的函数表,静态字段等等,也就是说其实静态字段存储在全局唯一对应的类型对象中,而不是存在于此类new出来的实例对象中,现在就能很好的解释静态字段两点性质啦。

普通C#类—饿汉式

为了更好的实现代码复用,以下三种单例模式均会采用工具类的设计方式,即设计成通用的父类,想要实现单例模式的子类只需要继承相应的单例工具类即可!

namespace Common
{
    /// 
    /// 饿汉式单例模式通用父类
    /// 
    /// 
    public class Singleton<T> where T : Singleton<T> //添加泛型约束为T必须为其本身或子类
    {
        //防止在外部创建对象,破坏唯一性
        protected Singleton() { }
        public static T Instance { get; private set; }

        //静态构造函数,程序启动时调用
        static Singleton()
        {
            Instance = Activator.CreateInstance(typeof(T), true) as T;
            Instance.Init(); //初始化一次
        }

        /// 
        /// 可选初始化函数
        /// 
        protected virtual void Init()
        {

        }
    }
}
  • 提供虚函数Init,可以通过重写此Init进行类的初始化工作,无需使用构造函数防止多次调用。
  • 利用泛型延迟声明单例模式对象,子类通过继承此父类并将自身类型赋给单例即可轻松实现单例模式,where针对泛型T约束其必须为自身或子类。
  • 饿汉式的单例,即在程序开始时即将单例的static属性Instance进行初始化,饿汉式的两个特征
    • 饿汉式单例是线程安全的,static构造函数只可能运行一次
    • 饿汉式单例存在空引用的风险,如果在另一个类的static构造函数中引用了此单例,由于运行顺序关系可能还没执行到此单例即没实例化,就会报空引用错误。这一点在MonoBehavior脚本的单例中体现尤为明显!

使用方式举个栗子

using Common; //注意命名空间的引用
public class Test : Singleton<Test>
{
	private string str;
	protected override void Init()
	{
		base.Init();
		str = "Hello World";
	}
	
	public string SayHello()
	{
		return str;
	}
}


public class TestMono : MonoBehaviour
{
	private void Awake()
	{
		string str = Test.Instance.SayHello();
		Debug.Log(str);
	}
}

普通C#类—懒汉式

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Common
{
    /// 
    /// 单例类懒加载
    /// 
    public class SingletonLazy<T> where T:SingletonLazy<T>
    {

        private volatile static T instance;
        /*volatile修饰:编译器在编译代码的时候会对代码的顺序进行微调,用volatile修饰保证了严格意义的顺序。
        一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
		精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。*/

        private static object locker = new object();
        public static T Instance { get
            {
                //双重检查 避免多次访问线程锁和多线程访问冲突问题
                if(instance == null)
                {
                    lock(locker)
                    {
                        if(instance==null)
                        {
                            instance = Activator.CreateInstance(typeof(T), true) as T;
                            instance.Init(); //保证只调用一次
                        }
                    }
                }
                return instance;

            } }

        /// 
        /// 可选初始化函数
        /// 
        public virtual void Init()
        {

        }
    }
}

  • 懒汉式单例,即按需加载,第一次使用此对象时加载,懒汉式单例也存在一些特征
    • 懒汉式单例是非线程安全的,可能同一时间有多个调用,会违反单例全局唯一的特性。
      • 多线程冲突问题,自然想到使用锁去隔绝,保证同时只能有一个线程进行实例化,为了让第一个线程实例化后,后续被锁隔离的线程进入时不重复实例化需要再锁的内部进行一重检查判空。
      • 由于锁的性能消耗,当第一次实例化后,后续的调用请求无需再被锁阻塞后再判空,可以在锁外添加第二重检查判空。
    • volatile特性的为了防止极小概率发生的地址问题,大多情况下都可以忽略,如果要追求极致严谨还需要添加此属性,原因在注释中已经解释。

脚本类 — 饿汉 + 懒汉

namespace Common
{
    ///
    ///脚本单例类,负责为唯一脚本创建实例
    ///

    public class MonoSingleton<T> : MonoBehaviour where T:MonoSingleton<T> //注意此约束为T必须为其本身或子类
    {
        private static T instance; //创建私有对象记录取值,可只赋值一次避免多次赋值

        public static T Instance
        {
            //实现按需加载
            get
            {
                //当已经赋值,则直接返回即可
                if (instance != null) return instance;

                instance = FindObjectOfType<T>();

                //为了防止脚本还未挂到物体上,找不到的异常情况,可以自行创建空物体挂上去
                if (instance == null)
                {
                    //如果创建对象,则会在创建时调用其身上脚本的Awake即调用T的Awake(T的Awake实际上是继承的父类的)
                    //所以此时无需为instance赋值,其会在Awake中赋值,自然也会初始化所以无需init()
                    new GameObject("Singleton of "+typeof(T)).AddComponent<T>();
                }
                else instance.Init(); //保证Init只执行一次

                return instance;

            }
        }

        private void Awake()
        {
            //若无其它脚本在Awake中调用此实例,则可在Awake中自行初始化instance
            instance = this as T;
            //初始化
            Init();
        }

        //子类对成员进行初始化如果放在Awake里仍会出现Null问题所以自行制作一个init函数解决(可用可不用)
        protected virtual void Init()
        {

        }
    }

}
  • Mono脚本的单例相较于普通C#单例的一些变化点
    • Mono脚本的实例化方式不能靠new,而是要挂载到GameObject身上,如果在场景中预先挂载好则直接获取,如果未挂载则自动创建GameObject并挂载上去。
    • 提供按需加载和初始加载两种方式,按需加载仍在get中进行,而初始加载则不能在构造函数中执行了,而是在脚本生命周期的Awake函数中初始化。
    • 在自动创建物体的地方可能有人会疑惑instance赋值的问题,实际上创建GameObject后的AddComponent就会执行一次Awake如果再赋值Instance或Init就违反了单例的特性。
  • 使用此完善的Mono单例父类,同时实现饿汉和懒汉,无需担心因Awake等调用顺序造成的空指针异常,大胆的使用即可!

结尾

​ 单例模式是常用的工具类,有了这三个脚本即可在开发中遇到单例需求,直接继承即可,工具类可以大大提高开发的速度,且无需做很多重复的工作,后续笔者会逐步将自己开发过程中用到的工具类分享出来哒!

你可能感兴趣的:(Unity开发,C#,开发,单例模式,unity,游戏引擎)