委托
什么是委托?
- 现实中的委托: 3个室友赖床,听到了我要去食堂吃饭的消息,所以他们准备委托我给他们带饭
不需要亲自执行,全权委托第三方,处理各种事务。例如律师帮助雇主起诉别人,雇主委托侦探去调查自己的老婆昨晚为什么没有回家。室友委托自己带饭。
- 在程序中委托: 通过委托类型的变量,间接的去调用封装在委托内部的方法
委托是一种类,是一种引用类型的数据类型,委托创建的实例可以存储一个,或者多个方法的引用。
如何声明委托?
//声明了一个无返回值的委托,无任何参数的委托
public delegate void MyDelegate();
//声明一个返回类型为int的委托,参数为2个int类型
public delegate int MyDelegate2(int a,int b);
声明委托的位置?
//声明在类外部的委托
public delegate void MyDelegate1()
public class MyClass{
//声明在类内部的委托称之为嵌套类型
public delegate void MyDelegate2()
}
如何使用委托间接调用方法?
static void GoodMorning()
{
Console.WriteLine("Good morning");
}
static void GoodAfternoon()
{
Console.WriteLine("Good afternoon");
}
static void GoodEvening()
{
Console.WriteLine("Good evening");
}
static string SayHello(string name){
return "Hello "+name+"";
}
static int TwoSum(int a,int b){
return a*b;
}
public delegate void MyDelegate();
MyDelegate d1;
d1 = new MyDelegate(GoodMorning);
d1 += new MyDelegate(GoodAfternoon);
d1 += new MyDelegate(GoodEvening);
d1.Invoke();
- 首先声明一个委托类型MyDelegate,该委托【无返回值,无参】【定义的位置可以在类的外部或者类内部】
- 初始化一个委托类型的变量 d1
- 实例化 委托 d1
- 使用Invoke调用封装在d1内部的方法
MyDelegate(void () target)
上面中表示的是,需要穿一个目标方法target,该方法返回类型为void,并且参数为()也就是空参数,只要传方法名即可不用传参数。
如果调用有参数和有返回值的呢?
public delegate string MyDelegate1(string name);
public delegate int MyDelegate2(int a,int b);
MyDelegate1 d1;
d1 = new MyDelegate1(SayHello);
d1.Invoke("frank");
MyDelegate2 d1;
d1 = new MyDelegate2(TwoSum);
d1.Invoke(2,4);
使用委托间接方法时,要注意委托返回值类型和方法是否一致,委托参数个数、 类型是否和方法一致。
如何使用语法糖衣简化委托?
完整版本调用和使用委托:
public delegate void MyDelegate();
MyDelegate d1;
d1 = new MyDelegate(GoodMorning);
d1 += new MyDelegate(GoodAfternoon);
d1.Invoke();
语法糖衣简化版本:
public delegate void MyDelegate();
MyDelegate d1;
d1 = GoodMorning;
d1 += GoodAfternoon;
d2();
通过语法糖衣简化了实例化委托的过程以及调用的过程。
单播委托、多播委托是什么?
//单播绑定
d1 = new MyDelegate(GoodMorning);
//多播绑定
d1 += new MyDelegate(GoodAfternoon);
//解绑
d1 -= new MyDelegate(GoodAfternoon);
顾名思义,单播委托就是绑定一个方法,多播委托则是绑定多个方法,在上面的例子中使用了运算法 = 、+=来表现单播委托和多播委托。
使用委托的意义是什么?
可能会有人问,委托是间接调用方法,和直接调用有什么区别,执行的结果不都是一样的吗?有什么实际的意义?
那么究竟使用委托间接调用方法比直接调用好在哪里?
- 委托可以通过封装方法到委托变量,达到间接调用方法的目的
- 也就是说可以将委托变量作为一个参数传递到另外一个方法去执行。
- 达到了一种动态调用方法的效果
委托的缺点是什么?
-
多播委托很容易因为操作失误导致封装的方法被重置
在多播委托中使用 += ,一旦写成了 = 则导致 覆盖掉之前的引用,既重置了委托封装的引用列表
-
过度使用委托会导致内存泄漏:Memory Leak
多播委托会引用多个方法,而当这个方法是实例方法【非静态方法】的话,也就是说这个方法隶属于一个对象。
一旦我们使用委托引用这个方法的话,那么这个对象就必须存在于内存当中。即便没有其他地方引用这个对象,因为委托的关系,这个对象也不能释放。
因为一旦释放,委托就不再能够间接调用到这个方法了,所以委托可能导致内存泄漏【Memory Leak】。
随着泄露的内存越来越多,会导致性能下降,程序崩溃的风险。
-
滥用委托会导致可读性下降
在委托变量中封装方法,然后将委托变量作为一个参数传递给其他方法,会导致可读性大大下降。
Action委托/Func委托
想要间接调用方法,需要使用委托,那么是否所有委托都需要我们使用delegate关键字自己去声明委托类型吗?
其实在C#类库中已经为我们准备好了2个内置委托类型,百分之90%的情况下已经可以满足使用了,可以不用自定义委托。
//语法糖衣写法
Action a1;
a1 = GoodMorning;
a1.Invoke();
//完整版本写法
Func<string,string> b1;
b1 = new Func<string, string>(SayHello);
b1.Invoke("Frank");
- Action委托是无返回值、可以无参或者有参的泛型委托【参数最多可达16个】
public delegate void Action();
- Func委托是有返回值、可以无参或者有参的泛型委托【参数最多可达16个】
public delegate TResult Func<in T, out TResult>(T arg);
Action委托与Func委托的区别在于:一个没有返回值,一个必须要有返回值
什么是模板方法/回调方法?
模板方法和回调方法是将委托变量作为一个参数传递进方法。
模板方法:下面有一个SayHello的模板方法。
public string SayHello(string name){
return "Hello" + name;
}
//首先声明一个Func委托变量封装SayHello方法
Func<string,string> d1 = SayHello;
//将委托变量作为参数传入方法内。
MakeFriend(d1);
public void MakeFriend(Func<string,string> _func){
//可以灵活的间接调用委托内部的方法。
string greet = _func("Frank");
Console.WriteLine(greet);
}
回调方法:
public void GoHome(Someone){
//go home
}
//逛街
public void DateWithGirdfriend(Action<SomeOne> _action){
//接女友
//看电影
if(DateTime.Now >= 23:00)
//回家
_action(Someone);
}
模板方法和回调方法在我看来都是差不多的,都是将委托变量传进一个方法,模板方法一般是有返回值的,回调函数一般没有返回值,方法的调用与否取决于代码的逻辑
事件(Events)
当发生一些有趣的事件时,事件能使一个类或者对象去通知其他类、对象。
别的类、对象在接收到这个通知之后,就会对这个事件作出各自的响应。
____________发生了
游戏发布
玩家阵亡
写数学作业
被父母批评
闹钟响了
发送消息
表白成功
C#如何定义事件?
- 事件是一种类型成员
- 事件可以”发生“,通过通知其他的类【其他关注这个事件的类】,他们得到通知后,发生的效果,才是事件的功能。
事件的核心功能就是将事件的参数、也就是事件的相关信息,通知给那些订阅了这个事件的类或者对象。
事件如何通知?
女神失恋了,这件事情发生后,女神闷闷不乐,于是在朋友圈写道:“人生不值得”。
这条消息都所有关注女神的人看到了。
在上面这个故事对应到程序语言则是:女神【类】 失恋了【事件】 发朋友圈【通知】,被关注【订阅】的人看到
也就是说女神把失恋这个事件发布到朋友圈后,所有关注着女神的人都能够得到通知。而不关注女神的人则得不到通知。
类如何对事件做出响应?
易烊千玺【类】 发布了一首新歌【事件】,因为我关注了易烊千玺的微博【订阅】 所以我得到了消息【通知】,于是我买了他的新专辑【响应】
在程序里,我们在接收到事件的通知后,会调用某一个方法去响应,我们把这 个方法称之为事件处理器
事件5个组成部分/步骤
事件模式在构建和运作时的5个步骤
- 我【类】要有一个事件【成员】
- 一群别的类关心、订阅我的事件
- 我的事件发生了
- 关心这个事件的类们都被依次通知到了
- 被通知到的类,根据拿到的事件信息,做出相应
-
事件的拥有者:一定是一个类【或者说对象】
皮之不存,毛将焉附:没有事件拥有者【类、对象】也就没有事件
-
事件:核心功能室通知其他类,对象、做出响应
⭐事件不会主动发生、一定是由事件拥有者的内部逻辑触发的。
按钮事件也不会主动被触发,一定是执行点击操作之后,被动触发的
我们可以通过事件降低代码的耦合性,保护程序不会内存泄漏。
-
事件的响应者:订阅事件的类、对象,他们会根据自身的事件处理器【方法】,来处理事件
-
事件订阅:事件发生后,通知的一定是订阅了事件的对象们
事件处理器和事件之间的关系
不同的事件需要不同的事件处理器,本质上来说就是,事件处理器的返回值和参数列表是否和事件的委托类型一致。
☘️知识点连接:向委托变量中封装方法时,也要遵循委托类型的返回值、参数列表与方法是否一致。概念相同
从Timer了解事件
1. Timer timer = new Timer();
2. timer.Elapsed += Printer.MyAction;
//timer.Elapsed += new ElapsedEventHandler(MyAction);
timer.Interval = 500;
timer.Start();
3 . private void MyAction(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Hello");
}
4. timer.Elapsed = Printer.MyAction;
- 声明了一个Timer类
- 调用Timer类中写好的Elapsed事件,并且为这个事件订阅事件处理器
- 实现事件处理器
事件是Timer类的成员,Elapsed是Timer类中的一个委托类型 +=操作也就是给委托类型变量Elapsed封装方法,本质上和委托是一致的,要保持委托类型和目标方法【事件处理器】的返回类型、参数类型一致
- 事件修复了委托中多播委托=可能会将封装方法重置的缺点,所以禁止使用=,避免重置已经调阅了这个事件的、事件处理器们。
如何定义完整的自定义事件?
//事件模型的五个组成部分
//事件的拥有者【类】
//事件【event关键字修饰】
//事件的响应者【类】
//事件处理器【方法-受到约束】
//事件的订阅关系【+=】
一 . 声明委托类型
public delegate void OrderEventHandler(Object sender, EventArgs e);
- 命名规范
事件 + EventHandler
遵循这样规定的委托变量,默认是用来声明事件的。不会被用来当做参数进行传递
- 返回值
事件的核心功能是通知,所以返回类型是Void
- 参数列表
(Object sender, EventArgs e);
- 这里的sender是事件的拥有者,告诉是谁触发了这个消息,第二个参数类型可以是EventArgs的派生类,告诉我们传递过来的事件里面是一些什么样的内容
- 这里的EventArgs是C#提供的一个空的数据类型,无法储存数据。
- 自定义参数类型用以存储事件相关数据
public class OrderEventArgs : EventArgs
{
public int CoffeePrice { get; set; }
public string CoffeeSize { get; set; }
public string CoffeeName { get; set; }
}
- 用于事件参数存储的类都应该继承自EventArgs
- 遵循相应的命名规范: 事件 + EventArgs
二、声明委托类型字段
private OrderEventArgs orderEventArgs;
- 使用private 访问修饰符,使外部无法访问内部委托字段。
三、声明事件
//创建一个委托类型
public delegate void OrderEventHandler(object sender, OrderEventArgs e);
//创建一个委托类型的字段
private OrderEventHandler orderEventHandler;
//创建一个委托类型的事件
public event OrderEventHandler OnOder {
add {
**** OnOder += value; //添加事件处理器
}
remove {
OnOder -= value; //移除事件处理器
}
}
- 这里创建的事件使用event关键字修饰
- OnOder事件中我们使用add和remove控制事件的添加和移除
- 🌟也就是说这里的事件只是对委托字段的一个封装,限制了事件的添加和移除
- ⭐在这里事件和委托字段的关系,和字段与属性是十分相似的。
四、触发事件
public void Order(){
if(orderEventHandler != null){
OrderEventArgs e = new OrderEventArgs(){
CoffeePrice = 15,
CoffeeSize = "Tall",
CoffeeName = "Cab"
};
orderEventHandler(this,e);
}
}
- 事件只能由事件内部主动触发,无法被外部触发。所以触发方法要写在事件拥有者的内部
- 在类的内部定义Order方法进行触发,当orderEventHandler字段不等于空意味着有其他类订阅了这个事件,那么则触发事件。
语法糖衣声明事件
public delegate void OrderEventHandler(object sender, OrderEventArgs e);
public event OrderEventHandler OnOder;
public void Order(){
if(OnOder!= null){
OrderEventArgs e = new OrderEventArgs(){
CoffeePrice = 15,
CoffeeSize = "Tall",
CoffeeName = "Cab"
};
OnOder(this,e);
}
}
- 简化版本事件的声明很简便,事件看起来像是使用了event关键字修饰的委托类型的字段
- 这里在触发事件的时候也要使用看起来像字段的事件名来判断
- 根据事件声明的完整版本我们知道,事件根本不是字段,而是对委托类型的字段进行了封装
- 这样做的好处是限制外部的恶意调用,恶意访问。
EventHandler、EventArgs
- 在定义自定义事件的时候,我们必须要自定义委托类型吗?
其实C#已经帮我写好了一些基础的委托类型供我们调用
//C#中自带的委托类型
public delegate void EventHandler(object sender, EventArgs e);
//自定义的委托类型
public delegate void OrderEventHandler(object sender, EventArgs e);
当我们有特殊需要时,再自定义委托类型,别忘记要遵循命名规范:Order + EventHandler
- EventArgs是一个空类型,是不能传递参数的
所以那么如何自定义类型传递参数?
- 遵循命名规范:Event + Args
- 派生自EventArgs 这个类
public class OrderEventArgs : EventArgs
{
public int CoffeePrice { get; set; }
public string CoffeeSize { get; set; }
public string CoffeeName { get; set; }
}
这样我们就能够将OrderEventArgs 类型的变量传递到事件处理器了,使用语法 as 转化为OrderEventArgs类型,进行取值
internal static void PayBill(object sender, EventArgs e)
{
OrderEventArgs order = e as OrderEventArgs;
}