确保一个类只有一个实例,并为其提供一个全局访问入口
本节告诉你如何避免使用这一模式
概要
使用场景
- 游戏管理器:如游戏状态管理、关卡管理等。像在一个角色扮演游戏中,游戏状态管理器负责控制游戏的开始、暂停、结束等状态,单例模式能保证全局只有一个管理器来统一协调。
- 资源管理器:管理游戏中的各种资源,如图片、音频、模型等。避免重复加载资源,节省内存和加载时间。
- 音频管理器:控制游戏中的音频播放,如背景音乐、音效等。保证音频播放的一致性和协调性。
优点
- 全局唯一访问:确保全局只有一个实例,方便其他对象访问。
- 资源控制:避免重复创建对象,节省系统资源。
缺点
- 违反单一职责原则:单例类既负责创建实例,又负责业务逻辑,职责过重。
- 扩展性差:修改单例类的功能可能影响整个系统。
- 多线程问题:非线程安全的实现方式在多线程环境下可能创建多个实例。
6.1 单例模式
6.1.1 确保一个类只有一个实例
6.1.2 提供一个全局指针以访问唯一实例
通常实现方法(线程不安全)
class FileSystem
{
private static FileSystem _instance;
public static FileSystem Instance
{
get
{
_instance ??= new FileSystem();
return _instance;
}
}
private FileSystem() {}
}
6.2 使用情境
优良特性:
- 如果我们不使用它,就不会创建实例
- 它在运行时初始化。包含静态成员/方法的静态类,是此模式的替代品,但静态类有个局限:自动初始化。编译器早在主函数调用之前就初始化静态数据了。这意味着
- 不能利用那些只有运行起来才知道的信息
- 这些静态类之间不能相互依赖——编译器不能保证它们的初始化顺序
- 你可以继承单例,这是个强大但被忽略的特性。假设我们需要跨平台的文件系统封装类。 为了达到这一点,我们将它实现为一个抽象接口,而由它的子类为每个平台提供实现。 这是基类:
abstract class FileSystem
{
private static FileSystem _instance;
public static FileSystem Instance
{
get
{
#if PLATFORM == PS3
_instance ??= new PS3FileSystem();
#elif PLATFORM == Wii
_instance??= new WiiFileSystem();
#endif
return _instance;
}
}
protected FileSystem() { }
public abstract void OpenFile();
}
之后我们为不同平台定义派生类
class PS3FileSystem : FileSystem
{
public override void OpenFile()
{
Debug.Log("Open file in PS3");
}
}
class WiiFileSystem : FileSystem
{
public override void OpenFile()
{
Debug.Log("Open file in PS3");
}
}
6.3 后悔使用单例的原因
6.3.1 它是一个全局变量
随着游戏变得更大更复杂,架构和可维护性开始成为瓶颈。阻碍我们发布游戏的不再是硬件,而是开发效率
- 它们令代码晦涩又难懂。若一个出问题的函数中引入全局状态,我们将要花更多时间来排查原因
- 全局变量促进了耦合
- 它对并发不友好。当设置全局变量时,这一块内存每个线程都可以访问和修改它。这有可能导致死锁、条件竞争等线程同步 bug
6.3.2 它是个画蛇添足的解决方案
- 确保一个单例是很有用,但谁说我们希望任何人都能操作它?
- 全局访问是很方便,但对于允许多实例的类,访问也并不麻烦。
我们通常会将日志类变为单例,起初只写一个日志文件时并无问题。随着开发的深入,我们希望可以通过将日志分割为不同的文件来诊断问题,显然单例的日志类不能胜任这个工作,要修改也十分麻烦。
6.3.3 延迟初始化剥离了你的控制
实例化一个系统需要花费时间,若我们在首次播放声音时实例化音频系统,且遇到游戏步入高潮时,这将会导致掉帧和卡顿
6.4 那我们该怎么做
6.4.1 看你究竟是否需要类
- 避免创建那么多的“Manager”,来帮助对象实现本来属于它们自己的功能
6.4.2 将类限制为单一实例
6.4.3 为实例提供便捷的访问方式
在盲目地采用具有全局作用域的单例对象前,让我们考虑下访问一个对象的其他途径:
- 作为参数传递进去
- 在基类中获取它
- 通过其他全局对象访问
- 通过服务定位器来访问
6.5 剩下的问题
我们应该在什么情况下使用真正的单例呢?老实说,我没有在任何游戏中使用 GoF 实现版本的单例。为了确保只实例化一次,我通常只是简单地使用静态类。若那不起作用,我就会用一个静态标识位在运行时检查