Uncategorized

对委托的理解

0.前言

日常开发中,有许多开发者因为这样或那样的原因选择疏远委托,其中比较常见的原因是因为委托的语法奇怪而对委托产生了抗拒感。但是掌握委托在Unity开发中又是非常必要的。接下来我会介绍一下委托相关概念,也算是对自己的知识进行了一遍梳理。

1.内容概述

这篇文章会主要介绍以下几个概念

  • 简单描述什么是委托
  • 委托和观察者模式之间的关系
  • Unity中的消息机制
  • 委托的协变性和逆变性
  • 委托的第一种简化方法:Action、Func
  • 委托的第二种简化方法:匿名方法
  • 委托的第三张简化方法:lambda表达式
  • 委托和事件(Event)之间的关系
  • 事件简介

2.委托的相关简介

简单来说委托是一种回调机制

C#通过委托来实现回调函数

回调函数是一种很有用的编程机制,可以被广泛应用在观察者模式中

观察者模式定义了对象之间,一对多的关系,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新

实际上我理解的设计模式是用一种统一的思想来解决问题的,并且根据语言的特性不同,实现起来也会有差异,并不用非要遵循经典的观察者模式,当然使用经典的设计模式会有更好的移植性

话说回来,委托也就是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 delegate void MyHandler(int a);

我们可以使用这个新创建的引用类型来声明实例变量

这里我们来使用一个栗子方便对比理解

        // 声明了一个委托类型的实例变量
	public MyHandler myHandler;
	
	// 声明一个类的实例变量
	public TestClass myTestClass;

接着我们又声明了两个跟委托类型具有相同签名的方法(返回值类型和参数类型相同)

void PrintNum(int a)
{
	Debug.Log(a);
}

void PrintNumDouble(int b)
{
	Debug.Log(b * 2);
}

最后我们在start方法里把具有相同签名的方法赋值给了委托实例,然后直接进行了方法回调

	private void Start()
	{
		// 一对一依赖
		myHandler = PrintNum;
		myHandler(10);
		myHandler = PrintNumDouble;
		myHandler(4);		
	}

可以看到打印日志

我们再来举一个委托一对多关系的例子

using UnityEngine;

public class DelegateTest : MonoBehaviour
{
	// 声明一个委托类型
	public delegate void MyHandler(int a);
	
	// 声明了委托类型的实例
	public MyHandler myHandler;

	private void Start()
	{		
		// 一对多依赖
		myHandler += PrintNum;
		myHandler += PrintNumDouble;
	}

	private void Update()
	{
		if (Input.GetMouseButtonDown(0))
		{
			myHandler(5); 
		}
	}

	void PrintNum(int a)
	{
		Debug.Log(a);
	}

	void PrintNumDouble(int b)
	{
		Debug.Log(b * 2);
	}
}

可以看到这次我们在start方法里采用了“+=”的方式,这样一个委托实例就可以监听多个方法回调。

然后单击鼠标左键测试可以看到如下结果

我们还可以在别的脚本上也添加对委托实例的监听

using UnityEngine;

public class CallBackTest : MonoBehaviour 
{
	private void Start()
	{
		GetComponent<DelegateTest>().myHandler += PrintReceive;
	}

	private void PrintReceive(int a)
	{
		Debug.Log("reveice : " + a);
	}
}

测试结果如下


3.Unity中的消息系统

既然提到了委托与观察者模式,那么Unity中是否已经存在了消息机制呢?

答案的是肯定的,这套内置的消息机制主要围绕着SendMessage和BroadcastMessage而构建。

但是这套机制是存在一些缺陷的

1、发送和接收消息都过于依赖反射来查找消息对应的被调用函数,频繁使用反射自然会影响性能。

2、使用字符串来标识一个方法会带来很高的维护成本,比如方法名字重构甚至删除了,编辑器是不会报错的。

3、由于使用了反射机制,是可以调用私有方法的,很多人可能会因为看到了私有方法没有被调用过而删除了这段废弃代码,同样编辑器并不会报错,甚至程序也能正常运行,但是如果触发了这个消息,隐患就会爆发。

综上所述,我们还是使用C#的委托来实现一个自己的消息机制比较好。


4.委托的三种简化方式

1、Action和Func

委托这种机制每次使用之前都要先创建一个新的引用类型,然后再创建实例,会显得比较臃肿、麻烦,所以C#提供了一种简化方式,使用Action和Func来创建委托实例

我们可以做一下对比

        // 声明一个委托类型
	public delegate void MyHandler(int a);
	// 声明了委托类型的实例
	public MyHandler myHandler;

	// 声明一个action类型的实例
	public Action<int> myHandler2;

可以看到一个需要两行代码,一个需要一行代码

那Action和Func有什么区别呢?

Action提供的是无返回值的委托类型,它提供了从从无参数到最多5个参数的定义形式

而Func提供的是有返回值的委托类型,在Action的基础上,每种形式又指定了一个返回值类型

举个栗子

// 声明一个委托类型
	public delegate void MyHandler(int a);
	// 声明了委托类型的实例
	public MyHandler myHandler;

	// 声明一个action类型的实例
	public Action<int> myHandler2;

	public Func<int, int> myHander3;

	private void Start()
	{		
		myHandler2 += PrintNum;
		myHander3 += PrintNumDouble;
	}

	private void Update()
	{
		if (Input.GetMouseButtonDown(0))
		{
			myHandler2(5);
			myHander3(10);
		}
	}

	void PrintNum(int a)
	{
		Debug.Log(a);
	}

	int PrintNumDouble(int b)
	{
		Debug.Log(b * 2);
		return b * 2;
	}

验证结果如下

2、匿名方法

首先匿名方法的价值在于简化代码

之前介绍的Action和Func简化了委托的声明过程,而匿名方法则简化了委托对应的方法声明,这样我们在处理简单逻辑的时候,可以直接关注与实现部分,而不用经过一些繁琐的步骤

举个栗子

	private void Start()
	{
		
		// 将匿名方法用于Action委托类型
		Action<int> printNumAdd = delegate(int a)
		{
			int b = 3;
			Debug.Log(a + b);
		};

		printNumAdd(2);
	}

输出结果

这里做一个额外扩充,感兴趣的童鞋可以自己研究一下

内置委托类型结合匿名方法,可以完成一些常见需求

过滤和匹配目标:Predicate<T>:

排序:Comparison<T>

3、lambda表达式

lambda表达式是匿名方法的进一步演化和简化,但是本身并非委托类型,不过它可以通过多种方式隐式或显式转换成一个委托实例。

举个栗子

               // 将lambda表达式用于Action委托类型
		Action<int> printNumDouble = (int a) =>
		{
			Debug.Log(a * a);
		};

		printNumDouble(3);

5.委托的协变性与逆变性

协变性指的是方法的返回值类型可以是从委托的返回值类型派生的一个派生类,协变性描述的是委托的返回值类型。

逆变性指的是方法的参数类型可以是委托的参数类型的基类,逆变性描述的委托的参数类型。

举个没什么实际意义的栗子方便大家去对比着看

	public class BaseClass
	{
		
	}

	public class MyClass : BaseClass
	{
		
	}

	public delegate Object MyHandler(MyClass myClass);

	public MyHandler myHandlder;

	private void Start()
	{
		myHandlder += TestMethod;
		myHandlder(new MyClass());
	}

	public GameObject TestMethod(BaseClass baseClass)
	{
		return gameObject;
	}

6.委托与事件

事件Event实质上是对委托的进一步封装,订阅事件的时候本质上是将委托类型的实例添加到委托列表中。事件只能被外部订阅,不能在外部触发,也就是对事件的只能监听“+=”、移除”-=”。

那为什么要对委托进一步封装呢?

我的理解是,事件保证了消息发送者的安全性和封装性,因为事件只能从内部方法触发事件,外部是触发不到事件的,只能订阅。


7.总结

通过委托,我们引出了回调机制,观察者模式等,并且介绍了委托的概念,简写方法,与事件之间的关系,为的是能让我们更好的理解委托,更方便的使用委托,减少对委托的抗拒心理,并且使用这种高效的机制实现我们的需求。对委托的理解