文章目录
- 委托
- 委托的定义
- 委托实例化
- 委托的调用
- 多播委托
- 为什么使用委托?
- 官方委托
- 泛型方法和泛型委托
- 事件
- 为什么要有事件?
- 事件和委托的区别:
- 题外话——委托与观察者模式
委托
在 .NET 中委托提供后期绑定机制。 后期绑定意味着调用方在你所创建的算法中至少提供一个方法来实现算法的一部分。
说的更简单一点,我们可以通过调用委托实现一大串方法的处理。委托像是一个装函数的容器,在我之前的文章里,将其比作了服务员点菜时写的小单子,当触发委托的时候,就是将单子交给后厨,厨师就会按顺序做出我们点的菜。
委托的定义
当我们定义委托时,需要使用到delegate
关键字。
delegate void MyFun();// 委托不可重载delegate void MyFun<T>();// 带泛型的函数名MyFun<T>和MyFun不同delegate int MyFun2(int i);
委托是不可重载的。无论修改函数返回值定义还是增加参数,只要存在相同函数名的委托就无法通过编译。(但是带泛型的不是相同函数名,例如MyFun,MyFun,MyFun<T,K>可以同时存在)
委托实例化
当想要使用委托的时候,需要将其先实例化,存在两种实例化方式,new一个对象并为其赋值第一个函数:
int Input(int i){Debug.Log(i);return i;}void Start()
{MyFun2 myFun = new MyFun2(Input);// new的时候需要定义第一个调用函数MyFun myFun1 = null;// 委托初始化可为空,但是空委托触发时会报错
}
需要注意:第一,委托的初始化必须放在方法中执行,不能在定义时初始化;第二,委托的格式和函数的格式必须完全相同。委托的返回值指定什么类型,赋给委托的函数就必须也是相同类型,而委托有多少入参,函数也要有同样的入参:
delegate int MyFun2(int i);
int Input(int i)
{return i;
}
void Input1(int i)
{
}
int Input2(string i)
{return 1;
}
MyFun2 myFun = new MyFun2(Input);
myFun += Input1; // 返回值不同,报错
myFun = new MyFun2(Input2); // 入参不同,报错
委托的调用
当调用委托的时候,我们需要确保委托不为空,可以使用反射和直接调用的方法来调用委托:
myFun1();//委托为空执行会报错
myFun(100);
myFun.Invoke(100); //带有入参的委托需要在调用时给出入参
myFun1.Invoke(); //委托为空执行会报错
myFun1?.Invoke(); //使用?.Invoke(),当委托为空时不调用
多播委托
委托可以包含多个方法并触发,这样的委托被我们称为多播委托,对于委托中的方法,可以简单地使用如下语句进行增减:
myFun1 += Fun;
myFun1 -= Fun2;// 当减去方法的时候,若委托中没有对应函数,
//编译(即使委托为空)和执行都不会报错
myFun1 += SayHi;
在多播委托中,委托中事件的触发顺序是按照执行语句时我们向委托中添加的方法的顺序来执行的,如果先添加A再添加B方法,则就是先执行A再执行B。
为什么使用委托?
请看下面的一个例子:
class Test{public MyFun fun;public MyFun2 fun2;int i=10;public void TestFun(MyFun fun,MyFun2 fun2){i= i*100;fun2.Invoke(i);fun.Invoke();}}
在上面的这个类中,我们使用一个函数来接收两个委托,随后在函数中处理了参数i
并定义了两个委托的调用次序。而最终,这个类在实例化后可以在需要时来调度这个函数方法。
当我们需要在类在实例化后处理一系列方法,例如实例化了一个Monster
类的小怪A
,i
对应它的伤害,fun1,fun2对应攻击后会触发的一系列反应。这样一个小怪攻击的方法就简单的完成了。
一方面,我们想要调用函数的话,首先函数是无法作为参数传入到其他函数中的,其次如果调度函数将考虑一大堆问题:例如需要调度的函数的访问修饰是不是public,是否要先引入其他的类,如果要修改这个攻击方式怎么办等等问题。使用委托就不用考虑这些问题,只需要保证委托和调用方法的格式是相一致的,将委托丢入方法后直接调用,把整个问题抽象到只考虑我们应该在什么时候触发委托就行了。
另一方面,在我们的结构中,触发的是委托而不是函数,也就意味着我们可以随意地修改攻击方法触发的函数,只需简单地对委托进行方法的加减。
我想介绍的委托的另一个使用例子来自unity官方,在官方提供的New InputSystem中,每个按键触发的就是委托。这使得我们可以像接口一样很简单地修改按键对应的方法。以往是用if
检测按键触发然后调度方法,现在我们可以通过委托把该调度的方法直接加入到委托多播上。这样更方便修改,也更灵活。
官方委托
虽然我们可以自行定义委托,但是毕竟代码是给人看的,可能有人接受你的代码后不知道对应的类型是类,结构体还是什么东西,只有当他查看引用了之后才知道这是一个委托。而官方很贴心的提供了一些有名字的委托:
Action action = test.Fun; // void Action() 无参无返回委托
Action<string> action1 = NewString; // void Action<T>() 有参无返回泛型委托,最多接受16个泛型传入
action1 += Tstring; // 如果函数同样接收泛型,那么函数的泛型会自动接受Action委托声明时给出的对应泛型
Func<int> func = Input;// T Func<out T>(); 无参带返回值泛型委托,使用out修饰代表该委托是协变的,最后一个泛型决定返回值类型
Func<int,string,int> func1 = Input;// Result Func<T1,T2...Result>(T1 arg1, T2 arg2...) 有参带返回值泛型委托,最多接受16个泛型传入
官方委托总共有两个:Action
代表了无返回值委托,Func
则是带返回值委托。而这两个委托又有同名泛型定义,最多接受16个泛型,每个泛型对应着一个入参。
使用它们的时候就和正常委托一样,举个例子:
void Input(){}
int Input()
{return 1;
}
int Input<T1,T2>(T1 a,T2 b)
{return 1;
}
void Tstring<T>(T i){}
void NewString(string i){}Action action = Input; // Action无入参无返回值,对应delegate void Action()
Action<string> action1 = NewString;// Action有入参无返回值,对应void Action<T>(T t)
// 上句对应的NewString类型也要完全一致,也是无返回值,入参一个,类型为string
Func func = Input;// Func无入参有返回值,对应TResult Func<out TResult>(),out修饰协变
// 注意当使用Func委托的时候,至少需要定义一个泛型,这个泛型对应的不是入参而是返回值的类型
Func<int,string,int> func1 = Input;// 同理,右侧最后一个泛型int代表了返回值的类型
// Func定义的三个泛型,则需要委托的方法要有返回值,且有两个入参
上述是官方委托的使用方法,当不需要返回值的时候使用Aciton
,当需要返回值的时候使用Func
,并且还需要定义最后一个泛型来代表返回值的类型。
泛型方法和泛型委托
int Input<T1,T2>(T1 a,T2 b)
{return 1;
}
Func<int,string,int> func1 = Input;
在上述代码中,函数Input
定义了两个泛型,而委托Func
中有三个泛型,实际上他们是匹配的,毕竟Func
中最后一个泛型代表了返回值的类型。
现在有下列定义:
T Input<T1,T2,T>(T1 a,T2 b)
{T t = default(T);return t;
}
Func<int,string,int> func1 = Input; // 报错
上述代码看起来很合理,三个泛型依次赋值给Input的三个泛型,实际上不行。Func
只会把前两个泛型定义给函数,因此第三个泛型编译器无法推断。
事件
学习了委托,事件其实也就学习了,事件和委托基本上一模一样:
class Test
{delegate void NewDel();event NewDel MyFun; // 注意,定义事件时其访问性必须与委托一致
}
在定义事件时,使用event
关键字来修饰委托名并命名出事件委托的定义,事件和委托的使用基本一模一样,就不再赘述了。唯一的区别在于事件是无法在类的外部进行赋值和调用的:
class Test{public delegate void NewDel();public NewDel del = null;public event NewDel MyFun;public Test(){del = NewFun;MyFun = NewFun;}public void NewFun(){}}void Start(){Test t = new Test();t.del();t.del.Invoke();t.MyFun(); // 报错t.MyFun.Invoke(); // 报错t.MyFun = null; // 报错t.MyFun += Appli;t.MyFun -= Appli;}void Appli(){}
在类的外部,不能直接操作Event
,只能进行加减函数的操作。并且事件也无法作为临时变量在函数中使用,智能作为成员存在于类和接口以及结构体中。
为什么要有事件?
- 防止外部随意置空委托
- 防止外部随意调用委托
- 事件相当于对委托进行了一次封装,使其更加安全
事件和委托的区别:
- 事件不能在外部赋值,在外部只能对其进行函数委托的加减操作
- 事件不能在外部执行,而委托在哪都能执行
- 事件不能作为函数中的临时变量,而委托可以
题外话——委托与观察者模式
首先简单介绍一下什么是观察者模式,观察者模式是对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
举个例子,现在有n个外贸公司,这些公司都有进口和出口的业务,当人民币贬值时,人民币贬值导致这些公司更倾向出口业务,而人民币升值则更倾向进口业务:
class Company{public void Update(bool 贬值了){if (贬值了) { Debug.Log("出口"); }else { Debug.Log("进口"); }}}class CHY{private List<Company> Companies = new List<Company>();bool state = false;void 贬值()// 原谅我不懂贬值的英文,幸好C#可以起中文名,作为举例足够了{state = true;Debug.Log("人民币贬值");}void 升值(){state = false;Debug.Log("人民币升值");}bool setState(){// 判断升值还是贬值的代码return state;}void getState(){foreach(var company in Companies){company.Update(state);}}}
在上述代码中,如果人民币发生增值和贬值,都会通过getState()
来通知那些观察者(公司),当观察者发现人民币汇率变化,则选择相应的战略计划。这就是一个简单的观察者模式。
在观察者模式中,明显这些相关联的类具有很强的依赖性,被观察者发生变化则会向所有观察者广播通知,而观察者则会做出相应的反应。
那么委托和观察者模式有什么关系呢?仔细想来,其实委托和观察者本质上是相似的,他们的处理模式都是:
启动——通知——逐一处理
现在让我们把上述观察者事件加入到一个委托当中,也就是:
Func<bool> ChangeState = setState;// 代码有点小问题,意思到了就行
ChangeState += Company1.Update;
ChangeState += Company2.Update;
......
ChangeState.Invoke(state);
当我们触发ChangeState
事件之后,人民币状态改变了,而委托中附加的公司也通知了。所实现的功能和观察者模式是一样的。那我们为什么要用委托实现呢?原因是解耦。
第一个例子中观察者和被观察者的关系是十分紧密的,以致于存在依赖,需要由被观察者来通知观察者。耦合性过高。而使用委托之后就无需定义被观察者中的getState()
方法,也无需定义List<Company>
,只需将观察者的更新状态方法添加到委托中即可。大大降低了两个类的耦合性。使用委托,被观察者完全不知道观察者的存在,这才是真正的观察者模式。