1.什么是单例模式
相信我们都遇到过这样的问题:
- 我们使用new创建对象,这个对象需要全局共享只用这一个
- 我们需要用一种简单的方式全局能访问一个类创建的唯一对象
总结下来,我们经常需要创建一个类的单例,也就是创建单个实例供全局访问。
2.单例模板
我们试图创建一种全局都可以访问的对象,拥有着简单的语法,像这样
class GameManager: Singleton<GameManager>
{
int val = 10;
public void Hi()
{
val++;
print(val);
}
}
//文件1
void TestMethod1()
{
GameManager.Instance.Hi();
}
//文件2
void TestMethod2()
{
GameManager.Instance.Hi();
}
我们按顺序调用TestMethod1和2.输出如下
11
12
我们来剖析单例模板下的奥秘,在代码里,我添加了较为详细的注释,而且单例模式作为最简单的设计模式,理解起来相当容易。
using System;
namespace AirFramework
{
public class Singleton<T> where T : Singleton<T>
{
private static readonly object _instanceLock = new object();
//volatile防止线程优化,多线程可能会对值的写入时机进行微调,导致冲突
private volatile static T instance;
private static bool initialized = false;
public static bool Initialized => initialized;
public static T Instance
{
get
{
//Double Check实现线程安全并提高性能,大多数时候不需要进入if内
if (instance == null)
{
//如果已经有线程进入,等待
lock (_instanceLock)
{
if (instance == null && !initialized)
{
instance = Activator.CreateInstance<T>();
initialized = true;
}
}
}
return instance;
}
}
public static void Load()
{
_ = Instance;
}
public static void Unload()
{
if (instance != null)
{
initialized = false;
instance = null;
}
}
//防止被new
protected Singleton() { }
}
}
同样的,我们对于MonoBehaviour类型的单例
也是类似的想法,但是不同的是mono类型不能使用new,而是采用addcomponent
using UnityEngine;
namespace AirFramework
{
public class SingletonMono<T> : MonoBehaviour where T : SingletonMono<T>
{
private static T instance = null;
private static bool initialized = false;
public static bool Initialized => initialized;
private static bool autoReset = false;
//Set this value will reset the Instance
public static bool AutoReset
{
get => autoReset;
set
{
if (value)
{
Unload();
}
autoReset = value;
}
}
public static T Instance
{
get
{
//不保证线程安全
if (instance == null && !initialized)
{
GameObject container = new GameObject();
container.name = "Mono(" + typeof(T) + ")";
instance = container.AddComponent<T>();
if (!autoReset) DontDestroyOnLoad(container);
initialized = true;
}
return instance;
}
}
public static void Load()
{
_ = Instance;
}
public static void Unload()
{
if (instance != null)
{
Destroy(instance.gameObject);
initialized = false;
instance = null;
}
}
protected SingletonMono() { }
}
}
但是这种单例模式,无法应对某些特殊情况,比如继承
假定存在GameManager类,BaseManager类,需要把GameManager做成单例,但是要继承BaseManager
我们可以发现继承单例模板和继承BaseManager冲突了
尽管这种继承式的单例是一种很脏的设计,但是没有最好的设计,只有最合适的。我们仍然要想办法解决
引出一种交单例属性的设计
这样就可以非侵入的实现这种很脏的设计
class BaseManager{}
class GameManager:BaseManager
{
public static GameManager Instance
{
get=>SingletonProperty<GameManager>.Instance;
}
}
下面是单例属性的模板,其实并没有太大差异
using System;
namespace AirFramework
{
public class SingletonProperty<T> where T : class
{
private static readonly object _instanceLock = new object();
//volatile防止线程优化
private volatile static T instance;
private static bool initialized = false;
public static bool Initialized => initialized;
public static T Instance
{
get
{
//Double Check实现线程安全
if (instance == null)
{
lock (_instanceLock)
{
if (instance == null && !initialized)
{
instance = Activator.CreateInstance<T>();
initialized = true;
}
}
}
return instance;
}
}
public static void Load()
{
_ = Instance;
}
public static void Unload()
{
if (instance != null)
{
initialized = false;
instance = null;
}
}
protected SingletonProperty() { }
}
}
缺陷
很多人都在讲“少用单例’,不用单例这种话,少有人思考为什么
大家一传十十传百形成了一种默契,绕的人头晕。
1.生命周期不可控
很多时候我们的单例是只有访问时才会加载的,属于懒汉式,这个单例的生命什么时候结束脱离了GC的掌控,只能靠我们自己去释放,如果忘记释放或者提前被释放容易产生各种问题,(其他开发者不知道你什么时候释放单例,所以就无法释放)
2.作用域过广
我们在全局都能通过静态访问单例,容易让用户误操作,而且让代码散乱不安全,如果有100个单例…这都是什么
3.单一职责过于复杂
使用单例可能会让一个类复杂多种职责,容易让单一文件过大,代码复杂性上升,一个GameManager统计分数还计时,还存玩家信息,又存地图信息…改起来亚历山大
我们应该辩证的对待单例,而非一味否定。
我觉得单例的问题主要是基于这三点,我们可以选择性的使用单例,比如第一点要注意单例的生命,只能靠自己去尽量克服,而第二点可以通过类的访问权限来限制外部不能访问单例类,第三点是单例违法设计原则的地方,适当的违反设计原则在中小项目会更方便高效。