参考
Unity游戏开发——对委托的理解
一、委托简介
委托也就是delegate是一个引用类型,他相当于一个装着方法的容器,他可以把方法作为对象进行传递,但前提是委托和对应传递方法的签名得是相同的,签名指的是他们的参数类型和返回值类型
using UnityEngine;
public class DelegateTest : MonoBehaviour
{
// 声明一个委托类型
public delegate void MyHandler(int a);
// 声明了委托类型的实例
public MyHandler myHandler;
private void Start()
{
// 一对一依赖
myHandler = PrintNum;
myHandler(10);
myHandler = PrintNumDouble;
myHandler(4);
}
void PrintNum(int a)
{
Debug.Log(a);
}
void PrintNumDouble(int b)
{
Debug.Log(b * 2);
}
}
声明了一个委托类型,可以粗暴的理解为我们创建了一个新的引用类型,我们可以使用这个新创建的引用类型来声明实例变量。
// 声明了一个委托类型的实例变量
public MyHandler myHandler;
// 声明一个类的实例变量
public TestClass myTestClass;
接着我们又声明了两个跟委托类型具有相同签名的方法(返回值类型和参数类型相同),最后我们在start方法里把具有相同签名的方法赋值给了委托实例,然后直接进行了方法回调
也可以一对多依赖
private void Start()
{
// 一对多依赖
myHandler += PrintNum;
myHandler += PrintNumDouble;
myHandler(5);
}
我们还可以在别的脚本上也添加对委托实例的监听
using UnityEngine;
public class CallBackTest : MonoBehaviour
{
private void Start()
{
GetComponent().myHandler += PrintReceive;
}
private void PrintReceive(int a)
{
Debug.Log("reveice : " + a);
}
}
二、消息机制
1.Unity中的消息系统
既然提到了委托与观察者模式,那么Unity中是否已经存在了消息机制呢?答案是肯定的,这套内置的消息机制主要围绕着SendMessage和BroadcastMessage而构建。但是这套机制是存在一些缺陷的
- 发送和接收消息都过于依赖反射来查找消息对应的被调用函数,频繁使用反射自然会影响性能。
- 使用字符串来标识一个方法会带来很高的维护成本,比如方法名字重构甚至删除了,编辑器是不会报错的。
- 由于使用了反射机制,是可以调用私有方法的,很多人可能会因为看到了私有方法没有被调用过而删除了这段废弃代码,同样编辑器并不会报错,甚至程序也能正常运行,但是如果触发了这个消息,隐患就会爆发。
2.使用C#的委托来实现一个自己的消息机制
参考unity-针对于消息机制的学习 一
消 息 类MMMessage
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 消息类
///
public class MMMessage {
//成员变量 发送消息的名字
public string Name{
get;
private set;
}
//成员变量 发送消息的消息主体
public object Boby {
get;
private set;
}
//构造函数 传值赋值
public MMMessage(string name,object boby){
Name = name;
Boby = boby;
}
}
消息名称列表MMMessageName
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 发送的消息名
///
public class MMMessageName {
public const string START_UP = "startUp";
}
消息控制中心MMMessageCenter
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 消息中心 消息控制类
///
public class MMMessageCenter {
//定义一个消息类委托 类型
public delegate void messageDelHandle(MMMessage message);
//定义消息控制类的单例
private static MMMessageCenter instance;
public static MMMessageCenter Instance {
get {
//若是instance为空,初始实例化一个消息控制中心
if (instance == null) {
instance = new MMMessageCenter();
}
return instance;
}
}
//**定义一个字典 <消息名称, 委托消息> 消息列表**
private Dictionary messageMap = new Dictionary();
///
/// **注册监听**
///
/// 消息名称
/// 消息内容
public void RigisterListener(string messageName, messageDelHandle handle) {
if (handle == null) return; //若是消息为空 退出方法
if (!messageMap.ContainsKey( messageName )) {
messageMap.Add( messageName, handle );
}//若是消息名称不存在,添加到消息列表
}
///
/// **移除/注销监听**
///
/// 消息名称
/// 消息内容
public void RemoveoListener(string messageName, messageDelHandle handle) {
if (!messageMap.ContainsKey( messageName ))
return; //若是消息列表里没有这个消息名称键值,已经注销直接退出
messageMap[ messageName ] -= handle; //若是存在,去除当前handle的消息内容
if (messageMap[ messageName ] == null) {
messageMap.Remove( messageName );
} //若是消息列表里当前消息数量为空,则清除该消息名称
}
///
/// **发送消息**
///
/// 消息名字
/// 消息主体,可以为空
public void sendMessage(string messageName,object boby = null) {
if (!messageMap.ContainsKey( messageName ))
return; //若是消息名称不存在 返回
messageDelHandle handle; //声明定义
messageMap.TryGetValue(messageName,out handle);
if (handle != null) {
handle(new MMMessage(messageName,boby));
}
}
}
使用
void Start () {
//发送消息
MMMessageCenter.Instance.sendMessage(MMMessageName.START_UP);
}
void Awake () {
//注册监听事件
MMMessageCenter.Instance.RigisterListener( MMMessageName.START_UP, StartUp );
}
private void StartUp(MMMessage message) {
Debug.Log("游戏启动");
}
private void OnDestroy() {
//注销移除监听事件
MMMessageCenter.Instance.RemoveoListener( MMMessageName.START_UP, StartUp );
}
三、委托的简化方式
1、Action和Func
委托这种机制每次使用之前都要先创建一个新的引用类型,然后再创建实例,会显得比较臃肿、麻烦,所以C#提供了一种简化方式,使用Action和Func来创建委托实例
using System;
using UnityEngine;
public class DelegateTest : MonoBehaviour
{
//// 声明一个委托类型
//public delegate void MyHandler(int a);
//// 声明了委托类型的实例
//public MyHandler myHandler;
public Action myHandler;
private void Start()
{
// 一对多依赖
myHandler += PrintNum;
myHandler += PrintNumDouble;
//myHandler(5);
myHandler.Invoke(5);
}
void PrintNum(int a)
{
Debug.Log(a);
}
void PrintNumDouble(int b)
{
Debug.Log(b * 2);
}
}
可以看到一个需要两行代码,一个需要一行代码
那Action和Func有什么区别呢?
- Action提供的是无返回值的委托类型,它提供了从从无参数到最多5个参数的定义形式
- 而Func提供的是有返回值的委托类型,在Action的基础上,每种形式又指定了一个返回值类型
using System;
using UnityEngine;
public class DelegateTest : MonoBehaviour
{
//// 声明一个委托类型
//public delegate void MyHandler(int a);
//// 声明了委托类型的实例
//public MyHandler myHandler;
public Action myHandler;
public Func myHander3;
private void Start()
{
// 一对多依赖
myHandler += PrintNum;
myHandler += PrintNumDouble;
//myHandler(5);
myHandler.Invoke(5);
myHander3 += PrintNumDoubleFunc;
myHander3 += PrintNumDoubleFunc3;
Debug.Log("TestFunc:" + myHander3(10));
}
void PrintNum(int a)
{
Debug.Log(a);
}
void PrintNumDouble(int b)
{
Debug.Log(b * 2);
}
int PrintNumDoubleFunc(int b)
{
Debug.Log(b * 2);
return b * 2;
}
int PrintNumDoubleFunc3(int b)
{
Debug.Log(b * 3);
return b * 3;
}
}
这里最后的返回值是30
Action< > :
- 无需定义委托类型
- 不能带有返回值,必须有参数
举两个栗子:
Action del = (a) => { }; //int为参数类型
del ( 1 ); //调用
Action del = (a,str) => { }; //int 和 string 都是参数类型
del ( 1 , "我是栗子" ); //调用
Fun< > :
- 无需定义委托类型
- 可以没有参数,必须有返回值
- 最后一个参数一定是返回值类型
举两个栗子:
//无参 int为返回值类型 , 花括号内必须有返回值且必须是int类型
Fun del = () => {return 9;} ;
del (); //调用
//带参 两个int钧为参数类型,boll为返回值类型,花括号内必须返回bool类型的值
Fun del = (a,b) => {return ture;};
del (1,2 ); //调用
2.匿名函数
首先匿名方法的价值在于简化代码
之前介绍的Action和Func简化了委托的声明过程,而匿名方法则简化了委托对应的方法声明,这样我们在处理简单逻辑的时候,可以直接关注与实现部分,而不用经过一些繁琐的步骤
private void Start()
{
// 将匿名方法用于Action委托类型
Action printNumAdd = delegate(int a)
{
int b = 3;
Debug.Log(a + b);
};
printNumAdd(2);
}
3.lambda表达式
lambda表达式是匿名方法的进一步演化和简化,但是本身并非委托类型,不过它可以通过多种方式隐式或显式转换成一个委托实例。
// 将lambda表达式用于Action委托类型
Action printNumDouble = (int a) =>
{
Debug.Log(a * a);
};
printNumDouble(3);
C# Lambda表达式
C# Lambda表达式详解,及Lambda表达式树的创建
在 2.0 之前的 C# 版本中,声明委托的唯一方法是使用命名方法。 C# 2.0 引入了匿名方法,而在 C# 3.0 及更高版本中,Lambda 表达式取代了匿名方法,作为编写内联代码的首选方式。
四、event关键字
1.参考知乎 unity的委托是什么? event 关键字有什么用?
委托是一个容器,可以放函数对象,并且可以触发委托面的每个函数调用。委托主要用户回调函数。
// 定义一个委托类型
public delegate void GreetingDelegate(int lhs, int rhs) ;
// 定义一个委托变量。
public GreetingDelegate MakeGreet;
// 触发容器里面所有函数调用
MakeGreet(3, 4);
我们如果在外部给委托变量加函数进来,那么委托要定义成public, 这样做又有一个问题,public外部的人也可以触发这个委托,如果我希望设计成外部可以加回调,但是只能是模块内部触发委托,那么我可以加一个event来修饰,这样虽然是public,但是外部无法触发委托,只能类的内部触发。
public event GreetingDelegate MakeGreet;
2.C# event关键字
using System;
namespace ConsoleAppTest
{
class Program
{
class Test
{
static void Main(string[] args)
{
FileUploader f1 = new FileUploader();
//委托设置为空
f1.FileUploaded = null;
f1.FileUploaded = Progress;
//重置委托
f1.FileUploaded = ProgressAnother;
f1.Upload();
//外部直接调用
f1.FileUploaded(6);
Console.Read();
}
}
class FileUploader
{
public delegate void FileUploadedHandler(int progress);
public FileUploadedHandler FileUploaded;
public void Upload()
{
int fileProgress = 5;
while (fileProgress > 0)
{
//传输代码,省略
fileProgress--;
if (FileUploaded != null)
{
FileUploaded(fileProgress);
}
}
}
}
static void Progress(int progress)
{
Console.WriteLine(progress);
}
static void ProgressAnother(int progress)
{
Console.WriteLine("另一个方法:{0}", progress);
}
}
}
以上调用者代码本身是和FileUploader类一起的,这起码存在两个问题:
1)如果在Main中另起一个线程,该工作线程则可以将FileProgress委托链置为空:
f1.FileUploaded = null;
2)可以在外部调用FileUploaded,如:
f1.FileUploaded(6) ;
这应该是不允许的,因为什么时候通知调用者,应该是FileUploader类自己的职责,而不是调用者本身来决定的。event关键字正是在这种情况下被提出来的,它为委托加了保护。
使用event的写法,如下:
添加event关键字后,上面提到的几种情况会被阻止。
static void Main(string[] args)
{
FileUploader f1 = new FileUploader();
f1.FileUploaded += Progress;
f1.Upload();
Console.Read();
}
五、知乎 Ivony C#的Delegate 为什么没在其他主流语言中普及?
首先是delegate的设计其实是有一些历史问题的,并不能说是最好的一种设计。
一个典型的问题就在于所有的delegate实例都是MulticastDelegate类型的,但事实上多播委托的使用范围并没有那么大。更有意思的是多播委托本质上是个串行委托,委托方法是一个接一个的执行的。而实际应用场景中我们会遇到并发多播,异步多播,当某个出现错误时继续执行其他方法的多播委托,所有这些都是MulticastDelegate搞不定的。所以变得意义不大。
到今天为止MulticatsDelegate和+=的运算符重载还是多用于事件处理,而事件用默认的多播委托实现还会有可能导致对象不被释放的坑。
其次就是delegate这个概念意义并不大,尽管在强类型语言里面我们的确需要发明一种东西来描述函数签名,并将单个函数签名固化成一种强类型。但绝大多数时候专门去强调这个概念的意义并不大。很多语言都支持这个特性,但是一般可以直接用函数来描述就可以了,不必另外发明一个委托的概念。
另外就是传统的delegate强类型还有一个缺陷,即使两个delegate类型所代表的函数签名是一模一样的,那他们俩也是两个类型。这在实际运用中是个麻烦。如果你需要用到两个函数库,而这两个库分别将某个类型的函数签名定义了一个委托,即使你只需要写一个函数就能满足两个函数库的要求,但你仍然不得不莫名其妙的创建两个委托实例分别给到两个不同类型的委托对象。
这个缺陷直接催生了Func和Action系列的委托。当Func和Action系列委托出现以及泛型委托类型参数的协变和逆变出现后,我们发现委托这个概念大部分时候变得很多余。也就是说我们可以轻松地写出很多代码根本用不着了解委托这个概念,我们最终的着眼点还是函数签名。
但是别忘了泛型和匿名方法是C# 2.0才出现的(省略委托实例创建表达式直接用方法名称代替委托实例也是2.0才引入的),而泛型委托类型参数的协变和逆变是C#3.0才出现的,C#一直在发展的过程中。还有大家所说的lambda表达式也是3.0才引入的。
无论怎样,现在设计一个语言在语言内部保留委托的概念是很正常的,但是再花时间去把这个概念作为亮点来介绍,以及专门去学习和阐述是没有什么意义的。即使是Java,其实那个SAM Type就是委托的别名,或者换句话说delegate就是SAM Type的别名和语法糖。
当然不管怎么样,C#的delegate语法相较于C/C++的函数指针的语法是一个巨大的飞越,而委托这种语法糖也远比所谓的SAM Type直观和简便。
当然我也看到很多人说委托的学习成本太高,我想说其实OOP和强类型编程语言的学习成本本来就略高于平均智商水平,早日发现并作出正确的选择是非常对的。