委托
委托的定义
委托是个类,分为Delegate自定义委托类型,Func有返回值的委托类型,Action无返回值的委托类型
Func和Action的底层原理便是用Delegate声明一个委托类型(有返回值和无返回值),并且通过泛型参数(最多十六个)来实现自定义参数类型和参数
其中,Func委托类型的最后一个参数为返回值
委托的两种使用情况:
-
模板方法(一般是用有返回值的Func)
就比如写作文的作文模板,同一个作文模板,通过修改其中的某些需要自己填写的句子,可以套用到不同的作文题目中。
在程序世界中,通过将委托类型的参数当作形参传入到函数中,通过将不同的函数赋给该委托类型作为形参,来实现调用该委托类型下的不同的函数方法,从而实现动态调用代码
传入该委托类型的形参便是现实世界中作文模板里某些需要自己填写的句子,模板方法当中,就只有传入该委托类型的形参是不确定的
当发生以上这种情况时,好几个函数只有一行代码(计算哪个数据的最大值)是不同的,其他逻辑都一样的情况,按照一般的写法,要用多少个函数,就得写多少个函数,很麻烦而且不利于代码复用,如果用委托的模板方法的话,只需要传入不同的函数赋给委托类型的形参,就可以实现不同函数的效果,增加或减少功能不用去修改到GetTopNum函数,只需要去增加一个小方法,提高了代码的复用性,减少了Bug
(将函数作为委托类型的形参传入的意思是将该函数赋值给该委托类型)
如果写成LAMBDA表达式的话,甚至都不用去增加一个小方法,只需要赋给委托类型一个匿名函数(不用写方法名的函数)即可
参数不用写类型是因为给LAMBDA表达式作委托类型的参数的时候,委托类型会自动进行类型推断
-
回调方法(一般是用无返回值的Action)
有这么一种方法,在想要调用的时候调用,在不想要调用的时候就不调用
事件
一、事件定义
事件是一种类型成员,事件有能力使一个类或者对象去通知其他类、对象们
事件是基于委托的,委托是事件的“底层基础”,事件是委托的“上层建筑”
委托类型定义了事件的有无返回值和参数类型,事件处理器必须和事件的有无返回值和参数类型一致,即双方都要遵守同一个约定(有无返回值和参数类型),我们把这叫做事件和事件处理器必须是匹配的
二、事件模型的组成部分
事件的拥有者:闹钟【类】 声明事件所在的类
事件:闹钟响了【event关键字修饰】 OnXXX
事件的响应者:舍友【类】 拥有具体方法XXX
事件处理器:起床【方法——受到约束的方法】 XXXEventHandler
事件订阅(+=操作符):舍友订阅了自己的闹钟响铃的事件【+=】 事件的响应者订阅事件
和委托相比的好处:
事件右边禁止使用“=”操作符,避免了使用多播委托的时候不小心将“+=”操作符写成“=”操作符的问题,避免了不小心重置了委托封装的引用列表的问题
事件的好处还有一点就是事件不能够在外部直接触发 customer.OnOrder(); ×
三、自定义事件
(1)声明该事件的委托类型:
在声明委托类型的时候,如果这个委托,是为了声明某个事件而准备的委托,那么这个委托的名字,就要去使用:事件名+EventHandler的格式,由于委托是一种引用类型,所以事件名首字母要大写
在定义事件参数的时候,即在定义该事件的委托类型的参数的类型的时候,要遵循:类型名+EventArgs这个格式
(2)声明事件:
声明事件的完整格式(用于理解事件内部结构、事件和委托的关系):
先声明该事件的委托类型的字段,再声明该事件
面试官如果问你:事件是不是委托类型的字段?是不是一种特殊类型的委托类型的字段呢?
一定要回答:不是不是
事件和委托就像妹妹和姐姐的关系,姐姐再像妹妹,她也不是妹妹
事件是委托类型字段的包装器,保护委托类型的字段不被外界所滥用,外界只能用“+=”和“-=”去访问它,也就是说,事件包含了委托类型字段的所有功能,但只是对外部暴露了“+=”和“-=”操作符。
总结:事件是用来“阉割”委托实例的,事件只能add、remove事件处理器,不能赋值。并且事件只能+=、-=,不能=、不能外部触发事件
类似的情况有字段和属性,属性是字段的包装器。字段能做的,属性都能做;属性能做的,字段不一定都能做
声明事件的简略格式(常用):
访问修饰符+ event +事件处理器(委托类型的实例、字段)+ 事件名称(一定要注意命名规范:On+事件名);
public event OrderEventHandler OnOrder;
简略声明事件后,我们并没有手动声明该事件的委托类型的字段,但实际上,后台帮我们声明了这个字段,用来存储事件处理器的引用
(3)触发事件(只能由事件拥有者触发)
使用声明事件的简略格式声明事件后,触发事件:
其中,OnOrder!=null是事件后不跟“+=”或“-=”操作符的一种特殊情况,因为使用声明事件的简略格式声明事件后,我们并没有声明该事件的委托类型的字段(其实是被隐藏起来了),无法判断这个字段是否为空,所以只能使用语法糖,用事件OnOrder取代原先这个位置上的委托类型的字段(orderEventHandler),来判断事件是否为空
语法糖是什么:
- 微软提供的委托类型 EventHandler
微软提供的用来传递事件数据的类EventArgs,
凡是用来传递事件数据的类,都是从这个类派生出来的
让自定义的传递事件数据的类继承EventArgs,就可以作为参数传入EventHandler委托类型了
将Object类型的变量转换为Customer类型的变量,我们可以用as操作符
-
面试题
(1)委托和事件的关系?
很多人都误会了委托和事件的关系,认为事件是委托类型的字段,这样是错误的。
这个误会的来源,一是很多书上只是一笔带过事件的简略声明格式,并没有去介绍事件声明的内部结构(一个添加事件处理器,一个移除事件处理器);二是很多人把官方文档中对事件的定义(看上去像一个委托类型的字段),当作是事件是委托类型的一个字段的意思;三是在添加或移除事件处理器的时候,用的是+=或-=,后面跟的是方法的名字,但其实我们也可以写成 “事件+= new 事件处理器(方法)”的格式,跟委托类型的字段去添加方法的引用是一样的,会让人误以为事件也是委托类型的字段,再加上判断事件是否为空时,我们使用的语法糖if(事件!=null)更会加深误会
事件其实是委托类型字段的包装器、限制器,限制外界对委托类型字段的访问。外界只能通过“+=”和“-=”两个操作符对事件进行添加事件处理器和移除事件处理器的操作,并不能去赋值和触发事件。事件是用来阻挡非法操作的“蒙版”,它绝对不是委托字段的本身
(2)为什么要使用委托类型来声明事件?为什么说事件是基于委托的?
视频中讲解的:
站在事件拥有者的角度来说,是为了表明事件拥有者,能够对外部通知什么样的消息;如果站在事件的订阅者、被通知的这个人的角度来看,是一种约定,事件处理器能够收到什么样的消息,也约束了我们使用什么样的方法,什么样的签名,来处理响应这个事件。并且,我们用委托类型的字段(委托类型的实例)可以去储存方法的引用,去储存事件处理器,当事件的响应者,向事件的拥有者,提供了一个与之匹配的事件的事件处理器之后,我们需要把这个事件处理器保存到某一地方。而能够记录、引用方法的这个任务,也只有委托类型的实例才能做到。所以我们在声明自定义事件的时候,使用的是委托类型。
自己总结的:
委托类型规定了事件拥有者和事件响应者通知和接收的消息必须是同一类型的消息,约束了添加和移除事件时必须要使用与之匹配(同样类型)的事件处理器,即使用与之匹配(同样类型)的方法来处理响应这个事件;并且事件响应者向事件拥有者提供了一个与之匹配的事件处理器之后,需要使用委托类型的字段(实例)来存储该事件处理器的引用,来记录该方法,存储该方法的引用
完整事件例子(顾客点单):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
/// <summary>
/// 事件的拥有者:顾客
/// 事件:点单
/// 事件的响应者:服务员
/// 事件处理器:计算最后金额
/// 事件订阅(+=操作符)
/// </summary>
namespace EventDemo
{
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);//声明一个委托类型
class EventExample
{
public static Customer customer = new Customer();
public static Customer customer2 = new Customer();
public static Waiter waiter = new Waiter();
static void Main()
{
customer.OnOrder += waiter.CalculateBill;
customer2.OnOrder += waiter.CalculateBill;
//使用事件,事件只能由事件拥有者触发,不能在外部去触发
//顾客1点了超大杯摩卡15+6 = 21
customer.Order("摩卡", 15, OrderEventArgs.CoffeeSizeEnum.Venti);
//顾客1点了中杯拿铁20
customer.Order("拿铁", 20, OrderEventArgs.CoffeeSizeEnum.Tall);
//顾客2点了中杯卡布奇诺
customer2.Order("卡布奇诺", 25, OrderEventArgs.CoffeeSizeEnum.Tall);
customer.PayTheBill();
customer2.PayTheBill();
/*//如果使用委托,不使用事件,委托类型的字段可以在外部进行调用,意味着顾客2可以把自己点的东西记在顾客1的账单上
//顾客1点了超大杯摩卡15+6 = 21
OrderEventArgs e1 = new OrderEventArgs();
e1.CoffeeName = "摩卡";
e1.CoffeePrice = 15;
e1.CoffeeSize = OrderEventArgs.CoffeeSizeEnum.Venti;
customer.OnOrder(customer, e1);
//顾客1点了超大杯拿铁20+6 = 26,并记在了倒霉蛋顾客1的账单上
OrderEventArgs e2 = new OrderEventArgs();
e2.CoffeeName = "拿铁";
e2.CoffeePrice = 20;
e2.CoffeeSize = OrderEventArgs.CoffeeSizeEnum.Venti;
customer2.OnOrder(customer, e2);
customer.PayTheBill();
customer2.PayTheBill();*/
Console.Read();
}
}
public class Customer
{
//声明一个点单事件
public event OrderEventHandler OnOrder;
/* //声明一个委托类型的字段
public OrderEventHandler OnOrder;*/
//输入prop后双击tab键自动生成属性,并且通过双击tab去切换类型和变量名
public float Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I have to pay : " + Bill);
}
/// <summary>
/// 点餐(咖啡名称、价格、大小)
/// </summary>
/// <param name="coffeeName"></param>
/// <param name="coffeePrice"></param>
/// <param name="coffeeSize"></param>
public void Order(string coffeeName,float coffeePrice, OrderEventArgs.CoffeeSizeEnum coffeeSize)
{
//语法糖:如果事件不为空(因为简略声明事件时委托类型的字段被隐藏了)
if (OnOrder != null)
{
OrderEventArgs e = new OrderEventArgs();
e.CoffeeName = coffeeName;
e.CoffeePrice = coffeePrice;
e.CoffeeSize = coffeeSize;
//事件只能由事件拥有者触发:限制只能自己给自己点单
OnOrder(this, e);
}
}
}
public class Waiter
{
//计算账单金额
public void CalculateBill(Customer customer, OrderEventArgs e)
{
float finalPrice = 0;
switch (e.CoffeeSize)
{
case OrderEventArgs.CoffeeSizeEnum.Tall:
finalPrice = e.CoffeePrice;//中杯,原价
break;
case OrderEventArgs.CoffeeSizeEnum.Grand:
finalPrice = e.CoffeePrice + 3;//大杯:原价+3元
break;
case OrderEventArgs.CoffeeSizeEnum.Venti:
finalPrice = e.CoffeePrice + 6;//超大杯:原价+6元
break;
}
customer.Bill += finalPrice;
}
}
public class OrderEventArgs
{
//咖啡是大杯、中杯还是小杯
public enum CoffeeSizeEnum { Tall,Grand,Venti}//默认为静态
public CoffeeSizeEnum CoffeeSize { get; set; }
//咖啡价格
public float CoffeePrice { get; set; }
//咖啡名称
public string CoffeeName { get; set; }
}
}
b站up链接(讲得很好):
【事件•语法篇】如何声明自定义的事件以及事件的完整/简略声明格式(附:事件与委托的关系,事件与属性的相似之处)_哔哩哔哩_bilibili