委托概述
很多时候,我们传入某个类型完整的实例引用,只是为了调用其中的一个方法吗,这时候我们就是需要委托
委托的应用场景
比如在排序过程中,我们会根据需要选择升序排列,降序排列,或者是字母顺序排列
为了减少重复代码,我们可以将比较方法作为参数传给排序函数,为了能将方法作为参数传递,我们要有一个表示方法的数据类型,也就是委托。
public static void BubbleSort(int[] items, ComparisonHandler comparisonHandler){
int i,j,temp;
for(i = item.Length; i>=0; i--){
for (j = item.Length - 1; j<= i; j++){
if(comparisonHandler(items[j - 1], items[j])){//和直接调用方法的语法一致
temp = items[j - 1];
items[j - 1] = items[j];
items[j] = temp;
}
}
}
}
public delegate bool ComparisonHandler(int first, int second);
public static bool GreaterThan (int first,int second){
return first > second;
}//方法签名的剩余部分(参数和返回值),必须兼容委托的签名
//...
//调用BubbleSort,并提供有委托捕捉到的方法名称作为实参
int[] items = new int[5];
BubbleSort(items,GreaterThan);//ComparisonHandler委托是引用类型,使用时不必用new实例化
Lambda表达式
应该优先使用Lambda表达式而不是匿名函数
语句Lambda
语句Lambda由形参列表,=>和代码块组成,避免了声明全新成员
比如上述的GreaterThan
方法就不必声明
BubbleSort(items,(int first, int second)=>
{
return fitst < second;
}
);
因为方法没有方法名不再是一个可访问成员,因此public修饰符和static修饰符都不需要了,此外编译器能根据委托推断出类型的话,也不必显式声明参数类型。
此外但只有单个参数,而且类型可推断时,可以省略参数列表的括号,这种有时会令人困惑。其余情况,包括没有参数,都需要圆括号。
IEnumerable<Process> processes = Process.GetProcesses().Where(
process => { return process.WorkingSet64 > 10000000000; });
表达式Lambda
表达式Lambda比语句更精简,可以将其中的=>理解为“满足…的条件”
BubbleSort(items,(first,second) => first < second);
值得注意的是,匿名函数并不与任何类型关联,只是能够转换成相应的委托类型,因此不能对其使用typeof()
方法,也只有转换完成后才可以使用 GetType()
注意事项
注意事项 | 例子 |
---|---|
由于Lambda没有类型,所以没有能从Lambda表达式中访问的成员,object的方法也不行 | string s = ((int x) => x).ToString(); |
Lambda没有类型,不能用于推断局部变量的类型 | var v = x => x; |
如果目的地在Lambda表达式之外,C#不允许在匿名函数内部使用跳转语句(break, continue,goto) | |
Lambda表达式引入的参数和局部变量作用域仅限于Lambda主体(其实就是匿名函数的再简化版) | |
Lambda表达式内部检测不到对外部变量进行初始化的情况 | int number;Func<string, bool> f = text => int.TryParse(text, out number); if(f("1")){} |
通用委托类型
为了减少自定义委托类型的必要,C#自带了一组通用的委托类型,System.Func
代表所有有返回值的方法, System.Action
代表返回void的方法。
public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate TResult Func<out TResult>();
public delegate TResult Func<int T,out TResult>(T arg);
//ComparisonHandler就能想这样声明
void BubbleSort(int[] items, Func<int, int, bool> comparisonMethod){...}
委托没有结构相等性
委托类型不仅具备结构相等性(structural equality),也就是说不能将某个委托类型的对象引用转换成不相关的委托类型,即使他们返回类型和形参完全一致。如果想要使用给定类型的委托,只能创建一个新的委托让他引用旧委托的Invoke
方法,new = old.Invoke;
但是我们能使用逆变和协变来对委托进行引用转换,比如 Action<int T>
中有一个in类型修饰符,可以将 Action<object>
的委托引用赋给 Action<string>
类型的变量
//public delegate TResult Func<int T,out TResult>(T arg);
Func<object, string> func1 = (object data) =>data.ToString();
Func<string,object> func2 = func1;
- 逆变性: 适合任何object的行动必定适合任何string
- 协变性:处理object的方法,也能处理string
委托中的循环变量
要避免在匿名函数中使用循环变量
Lambda表达式捕捉变量总是使用最新的值,而不是保留变量在委托创建时的值。捕捉一个循环变量时,每个委托都捕捉同一个循环变量,但这个变量发生变化时,捕捉他的每个委托都看到了变化。
因此在for循环中,for语句头中声明的任何循环变量在捕获时看成同一个外部变量。
static void Main(){
var items = new string[] { "Moe", "Larry", "Curly"};
var actions = new List<Action>();
foreach(string item in items){
string _item = item;//明确了每次循环都有新变量,每个委托捕捉的都是不同的变量
action.Add( () => {Console.WriteLine(_item); } );
}
foreach(Action action in actions){
action();
}
}
多播委托编码Observer模式
Heater和Cooler对象
class Cooler{
public Cooler(float temperature){
targetTemperature = temperature;
}
public float targetTemperature { get; set; }
public void OnTemperatureChange(float newTemperature){
if(newTemperature > targetTemperature){
System.Console.WriteLine("Cooler: ON");
}
}
}
class Heater{
//...
}
Thermostat类(发布者)向heater和cooler对象实例(订阅者)报告温度变化
public class Thermostat{
public Action<float> OnTemperatureChange{ get; set; }
public float CurrentTemperature {
get {return _CurrentTemperature;}
set {
if(value != CurrentTemperature){
_CurrentTemperature = value;
//再调用Invoke之前,使用null条件操作符检查null值
OnTemperatureChange?.Invoke(value);
}
}
}
private float _CurrentTemperature;
}
OnTemperatureChange
属性存储了订阅者列表,只需要一个委托字段就可以存储所有订阅者。也就是说同一个发布者的温度变化,会同时被Cooler和Heater接收
CurrentTemperature
负责设置和获取由Thermostat报告的当前温度, 在赋值之后使用 OnTemperatureChange?.Invoke(value);
就能向所有订阅者发布通知
连接发布者和订阅者
Thermostat thermostat = new Thermostat();
Cooler cooler = new Cooler(60);
thermostat.OnTemperatureChange += Cooler.OnTemperatureChange;
//签名相同的委托可以直接使用+=赋值
值得注意的是,既然委托时引用类型,为什么赋值给一个局部变量,在使用那个局部变量就能保证null检查线程安全性?
事实上,对于OnTemperatureChange -= <listener>
的任何调用都不会从OnTemperatureChange
中删除一个委托,而是会赋值一个全新的多播委托,原始的委托不受影响,所以局部变量不会反映出任何变化
- 委托并不能保证链表中的委托实例会被顺序调用,所以不应该让代码依赖于特定的调用顺序
- 如果一个订阅者引发了异常,那么链表后续的订阅者就不能正常收到通知,所以我们应该创建一个异常链表,在循环之后集中
throw new AggregateException
- 如果委托有返回值,由于顺序的不确定,我们无法使用哪个订阅者的返回值。所以和异常的模式一样,必须使用
GetInvocationList()
方法遍历每个委托调用列表来获取单独的返回值,所以一般原则是通过返回void避免这种情况
事件
委托结构由于封装的缘故存在缺陷(在之前的一篇博客提到过),我们需要event
关键词来增强封装
使用Event重建Thermostat
public class Thermostat{
public class TemperatureArgs : System.EventArgs{
public TemperatureArgs(float newTemperature){
NewTemperature = newTemperature;
}
public float NewTemperature{ get; set; }
}
public event EventHandler<TemperatureArgs> OnTemperatureChange = delegate{};
//定义事件发布者
public float CurrentTemperature {
//...
}
private float _CurrentTemperature;
}
OnTemperatureChange
的属性被移除,而且添加了event
关键词,即使被声明为public字段,它也提供了所需的全部封装。它禁止了为public委托字段使用=赋值操作符,而且只有包容类才能调用发布通知的委托。
我们声明事件时,使用的是空委托。这就可以引发事件,而不必检查null值。
编码规范
为了封装良好,我们只需要将原始委托声明更改为字段,再添加event关键词。此外为了规范还要替换为全新的委托类型,比如 Action<float>
替换为 EventHandler<TemperatureArgs>
,这是一个CLR类型,具体声明如下
Action<float>
// 等价于
public delegate void EventHandler<TemperatureArgs> (object sender, TEventArgs e)
where TEventArgs : EventArgs;
这个修改并不是C#编译器强制要求,但如果声明一个作为事件来使用的委托,规范是要求传递代表发送者和事件数据的两个参数。
调用方式完全一样,只是需要提供额外参数,如下列代码展示
public class Thermostat{
//...
public float CurrentTemperature {
get {return _CurrentTemperature;}
set {
if(value != CurrentTemperature){
_CurrentTemperature = value;
OnTemperatureChange?.Invoke(this, new TemperatureArgs(value));
//第一个参数包括调用委托的类的实例,如果事件时静态的则传递null值
//第二个参数是Thermostat.TemperatureArgs类型,他从System.EventArgs派生
}
}
}
private float _CurrentTemperature;
}
虽然应该优先使用 EventHandler<TEventArgs>
,而非使用自定义委托类型。但如果使用参数名有特殊意义,可以考虑一下
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTempreture);
public event TemperatureChangeHandler OnTemperatureChange;
内部机制
从本质上说,event关键字是编译器用于生成恰当封装逻辑的快捷方式
从CIL代码角度讲,C#首先获取原始事件定义,在原位置定义一个私有委托变量
有定义add_OnTemperatureChange()
和remove_OnTemperatureChange
两个方法,分别负责“+=”和“ -=”操作符。类似于实现属性的get,set代码块
代码实例讲解
为何要使用他们
当时看C#本质论中讲委托事件的章节时,当时或许清晰,不过过了一段时间还是不理解其中的本质
其实委托,事件在对设计模式有了一定的了解之后,就不难理解了
委托的本质就相当于一个函数指针,能够将方法作为参数输入,如同开头的排序一样,将具体的排序细节作为delegate传入就不用大段的swtich语句
事件一是为了保证正确的封装,二是为了类之间的解耦,不将方法定义在内部,而采用事件注册的方式
如何使用
尤其是事件,各种命名的规定和注册事件的方式确实比较难以理解。我们在具体场景中理解他们,知道了其本质,也就直到他们的运用了
很多教程都是用的什么银行、仓库的例子,他们的业务逻辑本来现实生活中就不方便理解,更何况用代码清晰的表示逻辑,更何况银行业务本来就很令人生厌,把这种情绪带入到学习事件中确实效果不佳
我们通过游戏当中的血量HP来讲解,我们定义玩家类型 Player
,分别包含 玩家ID,当前血量,最大血量,需要在数据上传服务器之前先对HP进行检查,同时检查的具体函数我们希望解耦合,因此我们采用事件注册的方式(看了代码之后再讲解)
为什么不直接放在
UploadData
内部呢,一是我们之后可能会修改或者增加HP的检查逻辑,比如玩家添加了能突破上限的BUFF,我们在获得这个buff之后在注册,比上传之前不断判断bool
值方便得多。二是可能涉及到比较复杂的计算,放在玩家类里边会占据很大的空间,阅读起来不方便,而且从设计模式上面讲,具体数据的计算应该是单独的类
public class Player
{
int playerID;
int curHP;
int maxHP;
// 定义构造函数
public Player(int playerID, int curHP,int maxHP)
{
this.playerID = playerID;
this.curHP = curHP;
this.maxHP = maxHP;
}
public void UpLoadData()
{
OnHPCalculating(new NumberCollectorEventArgs(this.curHP, this.maxHP));
}
// HPCalculating事件句柄的定义
public event EventHandler<NumberCollectorEventArgs> HPCalculating;
private void OnHPCalculating(NumberCollectorEventArgs e)
{
HPCalculating?.Invoke(this, e);
}
}
// 单独的数据存放容器类(继承System.EventArgs)
// 将其作为中间的过度体:将需要比较的数字先存放到这个class中
public class NumberCollectorEventArgs : System.EventArgs
{
public int leftNumber;
public int rightNumber;
public int otherNumber;
public NumberCollectorEventArgs(int l, int r, int o = 0) {
this.leftNumber = l;
this.rightNumber = r;
this.otherNumber = o;
}
}
// 测试使用类
public class Test
{
// 测试的主函数
public void Testing()
{
Player player = new Player(1, 100, 100);
player.HPCalculating += MaxHPChecking;
player.UpLoadData();
}
// 外部定义的max检查函数
void MaxHPChecking(object sender, NumberCollectorEventArgs e)
{
if(e.leftNumber > e.rightNumber)
{
e.leftNumber = e.rightNumber;
Console.WriteLine("当前血量已修正为上限:" + e.rightNumber);
}
else
{
Console.WriteLine("当前血量为:" + e.leftNumber);
}
}
}
这么多的类和变量还有古怪的命名,我们一个个的看
public event EventHandler<NumberCollectorEventArgs> HPCalculating;
这句定义的是事件具体的句柄,我们在 Testing
函数中,通过+=赋值,相当于是对传入函数的一个规定,这里指的是函数需要对 NumberCollectorEventArgs
进行处理。
public class NumberCollectorEventArgs : System.EventArgs
首先是这个古怪的命名,C#规定的是如果这个类是专门用于存储事件传递数据的,那么要在自定义的名称之后加上 EventArgs
,而且要继承**System.EventArgs
**,至于为什么有三个参数是因为等式判断会有修正参数的可能,比如误差的分析 e.leftNumber - e.rightNumber <= e.otherNumber
;
为什么有了HPCalculating
还有个 OnHPCalculating
,HPCalculating
用于外部添加订阅者,相当于一个委托列表。OnHPCalculating
是内部调用 HPCalculating
已经添加了的函数,HPCalculating?.Invoke
void MaxHPChecking(object sender, NumberCollectorEventArgs e)
这一坨参数是个啥?前面那个是事件的发送者,后面那个是事件参数并且为EventArgs
的子类。这么写是因为对于事件委托的规定,同时返回类型必须是void
使用之后的好处
我们花了这么多力气来实现了这个功能,就是为了以后可以简单快捷的添加注册事件
现在来了一个新需求,在上传数据之前需要根据是不是VIP来为玩家补充血量,我们不用去player类里面进行修改了,我们直接定义一个函数解决, 假设player已经实现了VIP判断和修改血量函数
public class Test{
public void Testing()
{
Player player = new Player(1, 100, 100);
player.HPCalculating += MaxHPChecking;
if(player.IsVIP){
player.HPCalculating += VIPBuff;
}
player.UpLoadData();
}
void VIPBuff(object sender,NumberCollectorEventArgs e){
var player = (Player)sender;
e.leftNumber += 20;
player.AddHP(e.leftNumber, e.rightNumber);
}
}
游戏当中不止有玩家,还有敌对生物,同样的我们对于敌对生物也能复用这些函数(当然指的是MaxHPChecking
,怪物哪有VIP,钱都冲到怪身上了)
在设计模式中理解委托与事件的具体运用,当我们遇到这种问题时,我们要想到运用委托和事件来解耦,来规范化我们的代码