C# 回看委托与事件

委托概述

很多时候,我们传入某个类型完整的实例引用,只是为了调用其中的一个方法吗,这时候我们就是需要委托

委托的应用场景

比如在排序过程中,我们会根据需要选择升序排列,降序排列,或者是字母顺序排列

为了减少重复代码,我们可以将比较方法作为参数传给排序函数,为了能将方法作为参数传递,我们要有一个表示方法的数据类型,也就是委托。

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还有个 OnHPCalculatingHPCalculating用于外部添加订阅者,相当于一个委托列表。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,钱都冲到怪身上了)

在设计模式中理解委托与事件的具体运用,当我们遇到这种问题时,我们要想到运用委托和事件来解耦,来规范化我们的代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值