这篇文章主要来讨论一下Unity中关于单例的使用,话不多说直接进入正题。
单例的特点
- 全局唯一,这是单例最重要的特点,在程序运行期间有且只有一个实例存在。
- 访问方便,由于单例是通过static方式实现,所以全局都可以访问。这一点有利有弊。
普通单例的实现
public class Singleton
{
//私有构造函数,防止外部创建实例
private Singleton() { }
//全局唯一实例
private static Singleton instance;
//获取实例的属性
public static Singleton Instance
{
get
{
//当实例对象为空创建实例,确保实例全局唯一性
if (instance == null)
instance = new Singleton();
return instance;
}
}
}
上面是一个普通单例的实现方式。将类的构造函数私有化,这样可以防止类的外部创建实例,通过静态属性在类的内部创建实例,并获取实例对象。通过上面的示例,了解到单例的基本实现方式。
泛型单例的实现
using System;
using System.Reflection;
//泛型单例基类
public class Singleton<T> where T : Singleton<T>
{
//泛型基类构造函数,用于检测子类构造函数访问权限
protected Singleton()
{
Type type = typeof(T);
ConstructorInfo[] ctros = type.GetConstructors();
if (ctros.Length > 0)
{
throw new Exception(string.Format("{0} has at least one accesible constructor.", type.Name));
}
}
private static T instance;
public static T Instance
{
get
{
if (instance == null)
instance = Activator.CreateInstance(typeof(T), true) as T;
return instance;
}
}
}
//泛型单例子类
public class TestSingleton : Singleton<TestSingleton>
{
private TestSingleton() { }
public void Init()
{
Debug.Log("TestSingleton init");
}
}
上面是一个泛型单例的实现方式,将单例对象的成员变量和属性获取抽象到基类中。这里有几点要说明的:
- 在这里没有采用new的方式创建实例对象,而是采用的反射的方式进行创建,是因为在子类中需要将构造函数标记为private,所以只能通过反射的方式创建对象。
- 由于没有很好的基类限制子类构造函数访问权限的方法,所以只能在每个子类编写的时候都重写一遍构造函数,将构造函数标记为private。但这样很容易造成遗漏,如果没有将构造函数标记为private,则可以在外部创建类对象。
- 为了防止子类遗漏重写构造函数,所以在基类构造函数中增加了检查子类构造方式的判断。虽然个人也觉得这种在运行时才去检查判断的方式不太好,但也是一种无奈的选择。最好的方式当然还是在编译阶段就对子类的构造函数进行访问权限限制,我在网上找了好多文章都没有看到很好的方法,如果好办法的小伙伴不吝赐教。
MonoBehaviour的单例实现
using System;
using System.Reflection;
using UnityEngine;
public class SingletonMonoBehaviour<T> : MonoBehaviour where T : SingletonMonoBehaviour<T>
{
protected SingletonMonoBehaviour()
{
Type type = typeof(T);
ConstructorInfo[] ctros = type.GetConstructors();
if (ctros.Length > 0)
{
throw new Exception(string.Format("{0} has at least one accesible constructor.", type.Name));
}
}
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
GameObject globalGameObject = GameObject.Find("GlobalGameObject");
if (globalGameObject == null)
{
globalGameObject = new GameObject();
DontDestroyOnLoad(globalGameObject);
}
instance = globalGameObject.AddComponent<T>();
}
return instance;
}
}
private void Awake()
{
GameObject globalGameObject = GameObject.Find("GlobalGameObject");
if (gameObject != globalGameObject)
{
GameObject.DestroyImmediate(this, true);
}
else if (globalGameObject != null && gameObject == globalGameObject)
{
T[] t = globalGameObject.GetComponents<T>();
if (t.Length > 1)
GameObject.DestroyImmediate(this, true);
else
instance = t[0];
}
}
private void Reset()
{
GameObject globalGameObject = GameObject.Find("GlobalGameObject");
if (gameObject != globalGameObject)
{
GameObject.DestroyImmediate(this, true);
}
else if (globalGameObject != null && gameObject == globalGameObject)
{
T[] t = globalGameObject.GetComponents<T>();
if (t.Length > 1)
GameObject.DestroyImmediate(this, true);
}
}
}
public class TestSingletonMonoBehaviour : SingletonMonoBehaviour<TestSingletonMonoBehaviour>
{
private TestSingletonMonoBehaviour() { }
public void Init()
{
Debug.Log("TestSingletonMonoBehaviour Init");
}
}
作为继承MonoBehaviour的单例,目标同样是保证全局唯一。除了不能用代码在类的外部创建实例对象之外,也不能通过在GameObject上绑定脚本的方式创建对象。所以这里对上代码做几点说明:
- Reset函数主要是防止编辑器模式下在“GlobalGameObject”以外的对象上绑定创建单例,以及防止多次绑定创建单例。这里的“GlobalGameObject”是我个人用于绑定创建单例的对象,小伙伴可以自行选择。
- Awake函数则是防止在运行时通过AddComponent<>()的方式创建单例,其目的还是保证单例的全局唯一性。
总结
关于Unity中的单例使用就暂时先介绍到这里,其中也有许多不完善的地方。包括如何在编译期让基类限制子类构造函数的访问权限的问题,如何在编译期不让单例使用AddComponent<>()添加单例的问题。有更好解决方式的小伙伴可以一起讨论。