单例模式简述
“确保一个类只有一个实例,并为其提供一个全局访问入口”。
单例模式不仅描述非常简单,代码也非常好写,类似这样:
class Singleton {
public:
static public Singleton* getInstance(){
if (instance == NULL){
instance = new Singleton();
}
return instance;
}
private:
Singleton(){}
static Singleton* instance;
}
单例模式确实带给了我们极大的方便,有些情况下,一个类有多个实例就不能正常工作。
比如一个操作文件的类,为了保证各步操作不互相干扰,我们封装的类就必须知道每步调用的操作,如果这个类有多个实例,那么我们就无法获知其他实例的操作。
被玩坏的单例
现在,我学会了一个很好的模式,我一定要在实际编程中使用它~
然后,灾难就来了:我需要一个XXX管理器,在游戏的很多地方都要使用它,对了,使用单例模式就轻松解决了!
于是,在代码中就充满了单例,然而,单例一时爽,却有很多隐患。
虽然有单例模式这种看起来高端一些的名字,实际上,单例模式就是一个全局变量,全局变量的危害许多人都非常了解,但是套上了一层设计模式的面纱,就把它抛之脑后了,美其名曰:我再使用单例模式写代码。但是,随着游戏变得更大更复杂,这样的全局变量就会带来很多危害:
- 它们令代码晦涩难懂。假设我们正在跟踪其他人写的函数中的bug。如果这个函数没有使用全局状态,那么我们只需要将精力集中在理解函数体,和传递给它的参数就可以了。
- 全局变量促进了耦合。全局变量使得任何人都可以轻松的使用它,在一个不了解结构的新手完成开发任务时,很可能为了完成任务,使得本不应该产生耦合的地方使用了这个单例。
- 它对并发不友好。当设置全局变量时,我们创建了一段内存,每个线程都能访问和修改它,而不管它们是否知道其他线程正在操作它。这有可能导致死锁、条件竞争和其他一些难以修复的线程同步的bug。
难以控制的单例
如果你的单例会在需要的时候创建,会出现这样的问题:你无法得知单例创建的时间。
当你实例化一个游戏系统的时候,可能会花费较长的时间,作为用户,肯定不希望在玩游戏玩得正嗨的时候遇到卡顿和掉帧,因此,比较好的方法是在加载游戏的时候手动控制单例的初始化。
你还需要小心翼翼的处理单例初始化的顺序(如果某个单例与另一个单例存在耦合,当然,不存在耦合是最好的,然而,当系统非常庞大的时候,你很难保证这一点),让你的系统能够正常的运行。
LZ在做的游戏中有个读取配置的单例系统,在做另一个单例系统的功能的时候,又依赖这个读取配置的单例。悲剧的是,在退出游戏到登录界面的时候,会把原来的系统干掉,重新读取配置,然而,另一个单例却依然存在,在某些特殊的情况下,调用这个功能的时候,程序就会崩溃掉。
因此,LZ不得不将两个系统的生命周期理顺,在当时临近发版本不能发生大的改动的情况下,只能加上很多check的代码防止崩溃。
虽然这个例子中的情况并不会很常见,但LZ想说的是,这个系统本来就可以不用单例的方式编写,当你能很好的控制这个实例的生命周期的时候,以上出现的各种奇怪的问题就不会出现了。单例用起来简单,可是它会一步一步将你的系统变得更见复杂和难以管理,留下许多暗坑。因此,在你想使用单例的时候,最好想一想,是否一定需要这样处理。
Other
当然,完全不使用单例也不是很现实。单例依然很强大并实用,在使用中,始终保持谨慎是很重要的,在最需要的地方使用,而抛弃那些可有可无的方案,不让单例模式在代码中泛滥,会让程序更加健壮~
网友评论