Unity 设计模式-单例模式(Singleton)详解
设计模式
设计模式 是指在软件开发中为解决常见问题而总结出的一套 可复用的解决方案。这些模式是经过长期实践证明有效的 编程经验总结,并可以在不同的项目中复用。设计模式并不是代码片段,而是对常见问题的 抽象解决方案,它提供了代码结构和模块间交互的一种设计思路,帮助开发者解决特定的设计问题。
设计模式总共有23种,总体来说可以分为三大类:创建型模式(Creational Patterns)、结构型模
式(Structural Patterns)和行为型模式(Behavioral Patterns)。
创建型模式:工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式关注于对象的创建,同时隐藏创建逻辑
结构型模式:适配器模式、过滤器模式、装饰模式、元模式、代理模式、外观模式、组合模式、桥接模式关注类和对象之间的组合
行为型模式:责任链模式、命令模式、中介者模式、观察者模式、状态模式、策略模式、模板模式、空对象模式、忘录模式、迭代器模式、解释器模式、访问者模式关注对象之间的通信
设计模式的特点:
- 通用性:设计模式针对的是软件开发中常见的设计问题,适用于各种软件工程项目。
- 可复用性:设计模式可以在不同项目和环境下被重复使用,提高代码的可维护性和扩展性。
- 可扩展性:设计模式有助于让代码结构更加灵活,易于扩展和修改。
- 模块化:通过设计模式,可以减少代码的耦合性,增强模块间的独立性。
- 提高沟通效率:设计模式为开发者提供了一种通用的设计语言,使得团队成员能够快速理解并讨论设计方案。
单例模式(Singleton)
单例模式 (Singleton Pattern) 是一种创建型设计模式,保证一个类只有一个实例,并提供一个全局访问点来获取该实例。它通过控制类的实例化过程,确保系统中只有一个该类的对象存在。
在单例模式中,类的构造函数通常是私有的,防止外部通过 new 来创建对象,类内部维护一个静态实例,通过公共的静态方法提供访问。
单例模式的优点:
- 在内存中只有一个对象,节省内存空间。
- 避免频繁的创建销毁对象,可以提高性能。
- 避免对共享资源的多重占用。
- 可以全局访问。
适用场景:由于单例模式的以上优点,所以是编程中用的比较多的一种设计模式。我总结了一下我所知道的适合使用单例模式的场景:
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
- 以及其他我没用过的所有要求只有一个对象的场景。
单例模式注意事项:
- 只能使用单例类提供的方法得到单例对象,不要使用反射,否则将会实例化一个新对象。
- 不要做断开单例类对象与类中静态引用的危险操作。
- 多线程使用单例使用共享资源时,注意线程安全问题。
总之,单例模式 主要用于控制对象的实例化,确保系统中只有一个类的实例,并通过全局访问点来控制对象的使用。它适用于需要全局共享资源、统一管理的场景,如日志系统、数据库连接池等。尽管单例模式在某些场景下有助于提升系统的稳定性和效率,但也应谨慎使用,以避免全局状态管理复杂化或滥用全局访问带来的耦合问题。
在 Unity 中使用 单例模式
在 Unity 中,实现一个线程安全的普通类和MonoBehaviour 类的泛型单例时,必须考虑以下几点:
- 普通类单例:不能被
new
,并且在多线程环境下线程安全。 - MonoBehaviour 单例:由于 MonoBehaviour 的实例是通过 Unity 的
AddComponent
创建的,不能直接通过new
,也需要保证在多线程环境下的安全性。
普通型(new)泛型单例模式
public abstract class Singleton<T> where T : class, new()
{
private static T _instance = null;
// 多线程安全机制
private static readonly object locker = new object();
public static T Instance
{
get
{
lock (locker)
{
if (_instance == null)
_instance = new T();
return _instance;
}
}
}
}
MonoBehaviour 的泛型单例模式
私有构造函数
为了防止在类外部创建新的实例,将构造函数设为私有,这样其他类就不能直接通过 new 关键字来实例化该类。
using UnityEngine;
public class Singleton : MonoBehaviour
{
private static Singleton _instance;
// 私有构造函数
private Singleton() { }
// 静态访问方法
public static Singleton GetInstance()
{
if (_instance == null)
{
_instance = this;
}
return instance;
}
// 可选:添加其他功能和数据到这个单例类
}
静态字段访问
如果我们想直接使用 instance 这个变量,我们可以将 instance 定义为公共字段而不是属性。这样,在其他脚本中就可以直接通过 GameManager.instance 来访问它。
public class GameManager : MonoBehaviour
{
public static GameManager _instance;
private void Awake()
{
if(_instance!=null)
{
Destroy(gameObject);
}
else
{
_instance = this;
DontDestroyOnLoad(gameObject);
}
}
public void Walk()
{
// 实现 Walk 方法的代码
}
}
在上面的例子中, GameManager 类的 instance 字段被定义为公共静态。在 Awake() 方法中,如果 instance 为 null ,则将当前实例赋值给 instance ,否则销毁重复的实例。这样,我们就可以在其他脚本中通过 GameManager.instance 来访问 GameManager 的唯一实例。
单例使用方式
GameManager.instance.Walk();
这样就可以直接调用 Walk() 方法而无需加括号。请注意,使用这种方式时,确保在调用 GameManager.instance 之前, GameManager 类的实例已经被正确初始化。
两种方法比较
两种方法各有优缺点,取决于我们的需求和项目的规模。让我们来比较一下:
(1)使用静态方法:
优点:
易于理解和维护:使用 GetInstance() 等明确的静态方法,可以清楚地表明我们正在获取单例实例。
更好的封装:通过静态方法,可以对实例创建的逻辑进行更好的封装,确保在获取实例时进行一些初始化或其他操作。
更安全:可以更好地控制实例的创建过程,避免因不当的直接访问导致的意外行为。
缺点:
冗余代码:在使用单例的时候,可能需要多次写 GetInstance() 方法调用,造成一定程度的代码冗余。
(2)使用公共静态字段:
优点:
简洁:直接使用 GameManager.instance 来访问单例实例,代码更加简洁明了。
减少方法调用:省略了调用静态方法的过程,直接使用字段访问。
缺点:
可读性和维护性较差:在代码中,我们无法清楚地看出 instance 是来自单例模式的,初次阅读代码可能会不太容易理解。
可能不够安全:由于没有封装的控制,其他代码可能会直接修改或重置 instance ,可能导致单例实例状态的不稳定。
综上所述,如果我们更关注代码的可读性、维护性和安全性,推荐使用静态方法来获取单例实例。这种方式使代码更具意图,并且允许在获取实例时进行更好的封装和控制。
如果我们更看重代码的简洁性,并且确认在项目中不会出现意外的直接修改 instance 的情况,使用公共静态字段可能会更加方便。
不管选择哪种方式,确保单例的创建和初始化逻辑是正确的,并且在使用单例实例时要小心避免潜在的错误和异常。
今天是2024年11月23日
重复一段毒鸡汤来勉励我和你
你的对手在看书
你的仇人在磨刀
你的闺蜜在减肥
隔壁的老王在练腰
而你在干嘛?