最近在总结以前做过的项目中用到的技术,发现用过不少的单例模板。而这些单例模板针对使用的场景不同,还存在着一些区别。所以重新温习了一下这方面的知识,做一下总结以便以后也能更快的回忆起来。
单例模式的作用
- 保证一个类只有一个实例,并提供一个访问它的全局访问点。
单例的适用之处
- 当确保一个类仅有一个实例,并且需要提供一个全局访问点时
- 如果某个类需要频繁的创建和销毁,并且此过程开销比较大时
- 如果需要对某些资源进行统一的管理或共享时
C#中
c#中基本的单例使用
让类自身负责保存它的唯一实例,并保证该类没有其它实例可构建,然后提供一个可供访问唯一实例的全局访问方法。
Class Singleton
{
private static Singleton instance; //私有静态成员,存储单例实例
private Singleton {} //私有构造,不能在外部被创建
public static Singleton GetInstance()
{
if (instance = null)
{
instance = new Singleton();
}
return instance;
}
}
- 这里将GetInstance()设置为静态类型的原因在于,静态类型会被定义成静态成员。而静态成员属于类本身而不是类的实例,在内存中只有一份,通过类名可以直接访问。因为不能在外部创建实例,进而只能用此种方式调用。静态方法内只能使用静态类型成员,instance也必须为静态
- 因为静态成员存储在静态存储区,new创建的唯一类实例存储在堆区,因此在程序结束时,才会被销毁。而new返回的实例的引用存储在栈区,方法结束就会被销毁
- 这里的静态方法也可以进行替换,使用字段来实现也可以
...
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;
}
}
- 使用sealed防止发生派生,派生可能会增加实例
- readonly,表明只能在静态初始化或者构造函数中分配变量
- 在多线程下,CLR会确保只有一个线程进行对象初始化工作
- 仅保证提供线程安全的实例创建和访问,对于线程中的数据需要进一步保证安全性
- 因为执行顺序原因,可能会造成空引用
懒汉式单例类
在自己被第一次引用的时候,才会将自己实例化
基本的单例使用中的单例就属于懒汉式单例
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;
}
}
- 普通的懒汉式单例,存在线程安全问题,通过使用双重锁定来避免。第一次判断空用来判断是否已经创建过实例;加锁保证只有一个线程可以访问实例化语句;第二次判断空用来避免多个线程同时等待lock,lock结束后,导致有其它线程重复访问实例化语句,因此需要再次判空
- 延迟加载,在调用时才创建,避免不必要资源消耗
- 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;
- 可以在Awake中得到自身的this引用进行初始化
- 通过AddComponent<T>();进行初始化
- 通过FindObjectOfType<T>();进行初始化
- 通过反射动态创建对象
- 不继承MonoBehaviour的单例类
- 通过new来进行初始化
- 通过反射进行初始化
存在的问题
- 在多线程环境下需要处理并发访问的问题
- GC不能去控制释放,在程序结束之后才能销毁。有时需要手动控制释放
- 不符合单一职责原则,一个类负责很多功能
- 全局可以访问,可能会增加代码的耦合度
- 隐藏了构造函数,在依赖注入时比较麻烦
- 隐藏了类之间的依赖关系,在结构方面可能会不清晰
- 如果采用类加载进行初始化的方式,由于执行顺序原因,可能会导致空引用
最后对于唯一性方面的 一些补充