委托,就是字面意思了。比如函数调用就是一种委托,就是委托给函数去执行了。
(
更加特殊一点的函数调用就是具体符合某种情境下的委托给类的成员函数调用,下面这个例子:也可以不看==
class RealPrinter { // the "delegate" 受托人
void print() {
System.out.print("something");
}
}
class Printer { // the "delegator" 委托人
RealPrinter p = new RealPrinter(); // create the delegate
void print() {
p.print(); // delegation 委托过程
}
}
public class Main {
// to the outside world it looks like Printer actually prints.
public static void main(String[] args) {
Printer printer = new Printer();
printer.print();
}
}
)
再更加特殊一点的就是在设计模式中的使用,比如策略模式(之前看navmesh的时候总结过),状态模式(跟策略模式的类图一样),访问者模式(见最后),观察者模式等等。
因为设计模式很讲究封装,而封装以后,怎么调用,就会经常用到委托这个概念了。
策略模式:用策略类封装算法的变化,使用的时候委托给具体的策略类。
状态模式:用状态类封装行为的变化,使用的时候委托给具体的状态类。
访问者模式:略,访问元素,元素委托访问者。
观察者模式:observer委托subject,麻烦subject通知自己做某些事情。
总结“委托的概念”在这些设计模式中的应用,其实都是用了聚合来代替继承。
那本文介绍的是C#语言层面中的委托delegate(关键字和使用方法)和事件event。
第一部分介绍委托,先从一个平常用到的列子开始,然后介绍委托的继承体系以及一些常用的函数和一些简化形式——语法糖,最后介绍内置的一些委托类型以及需要注意的可能会造成内存泄漏的地方。
第二部分介绍事件,其实事件和委托是一样的,只不过事件封装了对委托的函数的调用做了以下限制。
第三部分介绍一下UnityEvent
目录
1.委托
委托就是函数调用,本质上C#中委托的实现 是在 CLR 中对 函数指针 的封装。
其实函数指针也可以理解成一种观察者模式。
1)具体委托类型的例子
1.先定义一个具体委托类型,用关键字“delegate”,比如名为AIEntityUpdater:
public delegate int AIEntityUpdater(AIEntity entity, float gameTime, float deltaTime);
是不是有点难理解,这句话其实是定义了一种类型(不是普通的class这么定义的),委托类型,这个类型继承自MuticastDelegate。下面会说到。
2.定义了类型以后,就可以定义一个该类型的变量了 _aiUpdater
private AIEntityUpdater _aiUpdater;
3.定了变量以后,就对该变量进行赋值了。实例化该委托类型的对象,用匿名方法给委托类型赋值——delegate关键字 输入参数() 函数体{}
_aiUpdater =
delegate(AIEntity entity, float gameTime, float deltaTime)
{
return entity.UpdateAI(gameTime, deltaTime);
};
ps:委托实例的创建之路——
C#1的繁琐(用new),
C#2的方法组隐式转化以及匿名方法以及逆变协变,
C#3的lamda表达式,
C#4的泛型的协变逆变
4.记住_aiUpdater 是一个类的实例,他有类方法。以下的调用,其实是调用了该类型的invoke方法。调用这个委托实例
public void IteratorDoAIUpdater(float gameTime, float deltaTime)
{
_aiUpdater(_entites[i], gameTime, deltaTime);
}
我的理解这就是在没有观察者,只有观察者的行为的观察者模式
用委托类型承载观察者行为的组合。是一种更加灵活的观察者模式。而无需用传统观察者模式中的框架定义特定的类和特定的函数和特定的subject。
2)具体委托类型的继承体系(有点像组合模式,UML符号介绍戳这里)
Delegate是一个类,有Combine和Remove成员函数,
MuticastDelegate继承自Delegate,并且一个Delegate的列表,Delegate[ ] invocationList
比如 delegate void StringProcessor(string input) 就是一个具体的委托类型,继承自MuticastDelegate,有 Invoke, BeginInvoke 和 EndInvoke 。
以上包含了subject类的所有的元素。见观察者模式。
这三个继承体系的类 有哪些常用的数据和函数:
- Delegate
System.Delegate有两个静态方法: Combine和Remove 是负责创建新的委托实例和移除的。
// 比如x y是委托实例(具体委托类型)
// 那么:第一个为简化形式,后面为编译器执行转换,调用的就是 Combine:
x += y; ——→ x = x + y; ——→ x = Delegate.Combine(x, y)
- MuticastDelegate
MuticastDelegate 有一个字段——Delegate[ ] invocationList——负责内部管理委托所维护的方法列表们。
- 具体委托类型
具体委托类型有 Invoke, BeginInvoke 和 EndInvoke 这三个函数。
而具体这三个函数形式如何,要看具体委托类型的定义,比如下面这个委托类型:
delegate void StringProcessor(string input)
StringProcessor这个委托的这三个函数 Invoke, BeginInvoke 和 EndInvoke如下:
// 输入参数和返回值均一样
public void Invoke (string x);
// 输入参数一样
public System.IAsyncResult BeginInvoke(string x, System.AsyncCallback callback, object state);
// 返回值一样
public void EndInvoke(IAsyncResult result);
其中 Invoke函数的用途 就是调用委托实例:
void PrintString(string x)
{
//......
}
StringProcessor proc;
proc = PrintString
proc("Hello"); // 被编译成 proc.Invoke("Hello"); 运行时调用 PrintString("Hello");
// 或者可以直接写成
proc.Invoke("Hello");
具体参考:https://www.cnblogs.com/kewolf/p/4695889.html
3)一些内置的封装具体委托类型:
- Action<TParameter>没有返回类型
本质为:public delegate void Action<T>(T obj);
- Predicate<T>有返回类型的
本质为:public delegate bool Predicate<T>(T obj);
- Comparison<T>它是IComparer<T>接口的委托版本,比较函数。
本质为:public delegate int Comparison<T>(T x, T y);
- Converter<TInput, TOutput>
- Func<TParameter, TOutput>
4)可能会造成内存泄漏
如果 委托实例本身 不能被回收,委托实例 会阻止他的目标被作为 垃圾回收而造成内存泄漏。如何造成泄漏呢:
比如 长命对象 把一个短命对象 作为他 委托实例的目标 , 那么长命对象间接容纳了对 短命对象 的一个引用,延长了短命对象的寿命。
因为Delegates hold a reference to a method, and (for instance methods) a reference to the target object the method should be called on.
(就是如果是类的方法的话,那么还有一个 target。)
2.事件
事件和委托 是 属性和字段的关系。通过event的背后的编译就可以看出来。
本质上是字段(委托),只不过封装了字段,变成了属性(事件),限制了对字段的操作。
1)定义
一般我们定义事件的如下(字段风格的事件):event关键字 + 具体委托类型(类型类型)+事件名字
(也就是在委托变量定义之前多加了一个 + event关键字。)
public event EventHandler MyEvent;
(EventHandler是一个具体委托类型)
2)编译器转换
编译器会将上述语句转化成一个具有默认的add/remove实现的事件和一个私有委托类型的字段,如下:
// 1. 封装的委托字段
private EventHandler _myEvent;
// 2.封装的 add 和 remove 方法
public event EventHandler MyEvent
{
add
{
lock (this)
{
_myEvent += value;// 调用的是 Delegate.Combine
}
}
remove
{
lock (this)
{
_myEvent -= value;// 调用的是 Delegate.Remove
}
}
}
3)事件的用途
限制对委托的操作,使其外部的类 只能调用add、remove(即+= -= )订阅和取消的操作。
而不能调用=,或者Invoke等,Invoke只能本类进行调用。
4)事件和委托的区别
- 委托:
可以直接赋值=
可以其他类调用(call)
- 事件:
只能通过+=和-=操作(别的类进行订阅和取消咯)
只能在类内调用,也就是Invoke。
http://www.cnblogs.com/wudiwushen/archive/2010/04/20/1703763.html
UnityEvent
也就是Unity中的event,主要是加强了在UnityEditor中的界面中的订阅和取消操作。
下面是一个例子,来自于《Unity3D游戏开发》
一般安装惯例都会把事件 定义成 小写 onXXXXX
单独定义了一个脚本
1)实现函数 OnCollisionEnter2D(这个是系统的自动调用的),实现的内容是触发onCollisionEnter2D 事件
2)那肯定实现定义一个onCollisionEnter2D 事件,供别的脚本订阅了
3)别的脚本订阅这个事件
这样,就不需要在Script_1 Script_2 Script_3 都定义OnCollisionEnter2D这个函数,取而代之的订阅onCollisionEnter2D事件。
类似的还可以定义一个 TriggerListerner。
以下的2D版本的,3D版本的就换一下对应的3D类就行了。
// 一般定义UnityEvent 都会使用Serializable序列化, 否则无法在Editor中显示
//[System.Serializable]
public class CollisionEvent : UnityEvent<GameObject, GameObject> { }
public class CollisionListener:MonoBehaviour
{
// 定义一个事件。如果定义了序列化可以通过界面选择需要哪些函数。否则的话就是在代码中,用AddListener 如Script_1 Script_2 Script_3等
public static CollisionEvent onCollisionEnter2D = new CollisionEvent();
// 脚本生命周期中的函数,被动的调用。我们把这个碰撞事件,不在这里处理,而是再抛出去,让订阅了这个事件的人自行处理。
private void OnCollisionEnter2D(Collision2D collision)
{
onCollisionEnter2D.Invoke(gameObject, collision.collider.gameObject);
}
}
//
public class Script_1:MonoBehaviour
{
private void Start()
{
CollisionListener.onCollisionEnter2D.AddListener(delegate (GameObject go1, GameObject go2)
{
Debug.LogFormat("{0}开始碰撞{1}", go1.name, go2.name);
});
}
}
public class Script_2 : MonoBehaviour
{
private void Start()
{
CollisionListener.onCollisionEnter2D.AddListener(func2);
}
void func2(GameObject go1, GameObject go2)
{
//
}
}
public class Script_3 : MonoBehaviour
{
private void Start()
{
CollisionListener.onCollisionEnter2D.AddListener(func3);
}
void func3(GameObject go1, GameObject go2)
{
//
}
}
PS:
UnityAction 是 Unity自己实现的事件传递系统
UnityEvent 负责管理 UnityAction, 提供AddListerner、 RemoveListener 等
访问者模式
其实比较抽象,适用的案例也说不太清楚
element那一层是组合结构
visitor是对组合进行操作
objectstructure是组合的遍历器
2次分派,具体的visitor作为参数,分派给element的成员函数。
element中的函数再以自己作为参数,分派给visitor的成员函数。
至此调用了element.Accept(visitor),就相当于调用了visitor.VisitConcreteElementA(element)
那么关键的第一次分派是怎么进行的,是通过objectstructure进行操作的。objectstructure管理着所有的element,当然也可以对element的成员函数进行调用了。