[Unity设计模式与游戏开发]单例模式
Posted 丁小未
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Unity设计模式与游戏开发]单例模式相关的知识,希望对你有一定的参考价值。
前言
单例模式是我们最常用的设计模式,面试的时候如果问任何一个开发者设计模式,单例模式估计是脱口而出吧,23中常见的设计模式之中并不是所有设计模式都是很常用的,而单例模式绝对是最常用的那一个。但如果真正面试深入问到单例模式,那你确定你真的了解嘛?常见的面试会让你现场写个单例模式,如果深入一点的问的话会问单例模式有几种实现方式?用代码实现并说出各个方式的优缺点?想必如果面试官真这么问的话,估计绝大多数人也hold不住吧,今天我就来深入的整理一下单例模式。
为什么要使用单例模式?
单例模式理解起来很简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫做单例设计模式,简称单例模式。
处理资源访问冲突
我们先来看一个例子,我们定义一个往文件中打印日志的Logger类
public class Logger
private FileWriter writer;
public Logger()
File file = new File("/Users/dxw/log.txt");
writer = new FileWriter(file, true); //true表示追加写入
public void log(String message)
writer.write(mesasge);
// Logger类的应用示例:
public class UserController
private Logger logger = new Logger();
public void login(String username, String password)
// ...省略业务逻辑代码...
logger.log(username + " logined!");
public class OrderController
private Logger logger = new Logger();
public void create(OrderVo order)
// ...省略业务逻辑代码...
logger.log("Created an order: " + order.toString());
上面代码会看到,我们创建了两个Logger对象,但写到同一个txt文件中,就有可能存在日志相互覆盖的情况,因为这里是存在资源竞争的关系,如果我们有两个线程同时个一个共享变量修改,往里面写数据,就有可能相互覆盖了。有人会想到解决这个问题就是加一个锁,但如果加对象锁并不能解决多个对象竞争同一个资源的问题,我们需要加类锁才行,这个时候我们会想到如果将Logger设计成单例就不会存在这样的问题了。
表示全局唯一类
在业务概念上,如果有一些数据在系统中只应该保存一份,那就比较适合用单例类。比如常见的配置信息。在系统中,我们只有一个配置文件,当配置文件被加载到内存后,以对象的形式存在,也理所应当只有一份,常见的就是游戏数据配表。再比如,唯一ID号码生成器,如果程序中有两个对象就会存在重复ID的情况,所以,我们应该将ID生成器设计为单例。
单例设计模式常见写法
- 饿汉式(静态常量)
//饿汉式(静态变量)
public class Singleton1
//构造器私有化,外部不能new,不写这个构造函数则会默认有一个公有的构造函数,外部就可以new,这样不符合单例模式
private Singleton1()
//在内部创建一个实例对象
private static Singleton1 instance = new Singleton1();
//提供一个公有的静态方法,返回实例对象
public static Singleton1 GetInstance()
return instance;
使用测试
Singleton1 instance1 = Singleton1.GetInstance();
Singleton1 instance2 = Singleton1.GetInstance();
Debug.Log(string.Format("instance1和instance2是否相等:0", instance1 == instance2));
Debug.Log("instance1的hashCode:" + instance1.GetHashCode() + " instance2的hashCode:" + instance2.GetHashCode());
测试效果
优缺点说明:
优点:
写法简单,在类装在的时候就实现了实例化,避免了线程同步的问题。
缺点:
在类装在的时候就完成实例化,没有达到Lazy Loading的效果。如果从开始至终都没有使用过这个实例,则会造成内存的浪费。
结论:
这种方式单例模式可用,可能造成内存的浪费。
- 饿汉式2
实例化的操作也可以放在私有构造函数内
public class Singleton2
private static Singleton2 instance;
//将实例化放在私有构造函数里面
private Singleton2()
instance = new Singleton2();
//提供一个公有的静态方法,返回实例对象
public static Singleton2 GetInstance()
return instance;
- 懒汉式
public class Singleton3
private static Singleton3 instance;
private Singleton3()
//提供静态公有方法返回实例对象
public static Singleton3 GetInstance()
//需要用到的时候再实例化单例对象,即懒汉式
if(instance == null)
instance = new Singleton3();
return instance;
优缺点说明:
1.起到了懒加载的效果,但只能在单线程下使用。
2.如果在多线程下,一个线程还没进入if(instance == null)的逻辑,另外一个线程也进行了访问,又进行了对象的创建就会产生多个实例。
结论:
在实际开发中,不要用这种方式,但Unity游戏开发一般不使用多线程的方式,所以Unity游戏开发中这种模式还是用的挺多的。
- 懒汉式(线程安全,同步方法)
public class Singleton5
private Singleton5()
private static readonly object syncObj = new object();
private static Singleton5 instance = null;
public static Singleton5 Instance
get
lock (syncObj) //添加同步锁
if (instance == null)
instance = new Singleton5();
return instance;
优缺点说明:
1.解决了线程不安全的问题
2.效率太低,每个线程想访问的时候都需要执行一次同步,如果一个线程加锁,第二个线程只能等待,效率太低。
结论:
实际开发中,不推荐使用这种方式。
- 懒汉式(双重检测)
public class Singleton6
private Singleton6()
private static readonly object syncObj = new object();
private static Singleton6 instance = null;
public static Singleton6 Instance
get
//双重检测提高效率
if (instance == null)
lock (syncObj) //添加同步锁
if (instance == null)
instance = new Singleton6();
return instance;
优点说明:
既解决了懒加载的问题,又解决了线程同步的效率问题。
总结:
在实际开发中推荐使用的方式。
Unity中常见的通用单例模式
Unity开发中最常创建单例模式的就是各种Manager,在程序启动的时候首先实例化各种单例的Manager,而我见过一个主程是这样写的,每一个Manager自己内部定义一个static xxx instance,然后在GetInstance方法中实例化这个instance返回,就会显得很重复,一般Unity中是这样创建通用泛型单例类的
创建非MonoBehavior单例
public class Singleton<T> where T : new()
protected static T _instance;
public static T sInstance
get
if (_instance == null)
_instance = new T();
return _instance;
我们使用的时候
public class BattleManager : Singleton<BattleManager>
因为Unity里面几乎不太使用多线程,所以这里就没考虑加锁的情况。
创建MonoBehavior单例
public class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
private static T _instance;
public static T sInstance
get return _instance;
protected void Awake()
if(_instance != null)
DestroyImmediate(gameObject);
return;
_instance = gameObject.GetComponent<T>();
InitOnAwake();
protected void OnDestroy ()
if(_instance == this)
ReleaseOnDestroy();
_instance = null;
protected virtual void InitOnAwake()
protected virtual void ReleaseOnDestroy()
使用
public class BattleCameraConfig : MonoSingleton<BattleCameraConfig>
顾名思义就是MonoBehavior的单例是可以挂在GameObject上的。
单例模式的弊端以及替代方案
1.单例模式存在的问题?
- 单例对OOP特性的支持不友好
OOP的四大特性是封装、抽象、继承、多态。单例这种设计模式对于其中抽象、继承、多台都支持的不好,举例说明:
public class Order
public void create(...)
//...
long id = IdGenerator.getInstance().getId();
//...
public class User
public void create(...)
// ...
long id = IdGenerator.getInstance().getId();
//...
IdGenerator的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的OOP的抽象特性。如果未来某一天,我们希望针对不同的业务采用不同的ID生成算法。比如,订单ID和用户ID采用不同的ID生成器生成。为了应对这个需求的变化,我们需要修改到所有用到IdGenerator类的地方,这样改动就会比较大。
public class Order
public void create(...)
//...
long id = IdGenerator.getInstance().getId();
// 需要将上面一行代码,替换为下面一行代码
long id = OrderIdGenerator.getIntance().getId();
//...
public class User
public void create(...)
// ...
long id = IdGenerator.getInstance().getId();
// 需要将上面一行代码,替换为下面一行代码
long id = UserIdGenerator.getIntance().getId();
除此之外,单例对继承、多态特性的支持也不友好。这里"不友好"并不是"完全不支持",从理论上讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,导致代码可读性变差。不明白设计意图的人,看到这样的设计,会觉得莫名其妙。所以,一旦选择将某个类设计成单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就是相当于损失了可以应对未来需求变化的扩展性。
-
单例会隐藏类与类之间的依赖关系
我们知道代码的可读性非常重要,在阅读代码的时候,我们希望一眼就能看出类与类之间的依赖关系,搞清楚这个类依赖了哪些外部类。通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。所以在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才知道这个类到底依赖了哪些类。 -
单例对代码的扩展性不友好
我们知道单例类智能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或者多个实例,那就要对代码有比较大的改动。举个例子,软件系统设计初期,我们可能会觉得系统中只应该有一个数据库连接池,这样能方便我们控制数据库连接资源的消耗,所以数据库连接池就被设计成了单例类。但之后发现,我们可能会用到好几种数据库,但不同的数据库的访问接口都不一样,这样我们就需要不同的连接池对象,也就是不能设计成单例类,实际上一些开源的数据库连接池也确实没设计成单例类,我一开始刚接触.NET开发的时候就碰到过MSSQL和Oracle之间的切换。 -
单例对代码的可测试性不友好
-
单例不支持有参数的构造函数
2.单例有什么替代方案?
为了保证全局唯一性,除了使用单例,我们还可以用静态方法来实现。不过静态方法这种实现思路,并不能解决上面提到的问题。如果要完全解决这些问题,我们需要从根本上寻找其他方式来实现全局唯一类,可以由程序员自己来保证不要创建两个类的对象。
设计模式系列教程汇总
http://dingxiaowei.cn/tags/设计模式/
教程代码下载
https://github.com/dingxiaowei/UnityDesignPatterns
以上是关于[Unity设计模式与游戏开发]单例模式的主要内容,如果未能解决你的问题,请参考以下文章