(八)委托与事件

1.什么是委托

什么是委托呢?委托的英文单词是delegate,从字面上来说,就是你有一些事情要处理,但是你不用自己动手,而是把它交代给别人替你去完成,也就是间接的完成了这个事情。

案例:CsharpStudy08
photo相片类,使用这个类可以加载或者保存一张相片。

 public class Photo
 {
     public static Photo load(string path)
     {
         return new Photo();
     }

     public void Save()
     {

     }
 }

在photoProcessor中我们使用了三个滤镜

 public class PhotoFilters
 {
     public void ApplyBrightness(Photo photo) { Console.WriteLine("亮度增加"); }
     public void ApplyContrast(Photo photo) { Console.WriteLine("对比度增加"); }
     public void Resize(Photo photo) { Console.WriteLine("图片放大"); }

 }
 public class PhotoProcessor
 {
     public void Process(Photo photo)
     {
         var filters=new PhotoFilters();
         filters.ApplyBrightness(photo);
         filters.ApplyContrast(photo);
         filters.Resize(photo);

         photo.Save();
     }
 }

主程序入口

 var photo = Photo.load("phtot.jpg");

 var processor = new PhotoProcessor();
 processor.Process(photo);

要如何处理,才能添加更多的滤镜呢?按照目前的代码逻辑,添加滤镜是一个比较麻烦的过程、而且代码的侵入性非常强,可以说,目前的代码是没有什么可拓展性的。

1.1 委托案例

打开图片处理器,我们需要在这里先给相片处理器定义一个滤镜处理的委托类型。public,使用delegate 关键词定义委托类型;然后,定义被委托方法的方法签名,也就是将要处理滤镜的方法;滤镜处理,方法返回void,名称PhotoFilterHandler,滤镜处理器,参数传入需要处理的照片,photo。好了,这就是我们滤镜处理的委托定义。
public delegate void PhotoFilterHandler(Photo photo);
图片滤镜都会通过这个 PhotoFilterHandler 来进行处理。图片的滤镜的定义过程将会被放在图片处理器外部,并且由图片处理器的调用方来负责加载。

 public delegate void PhotoFilterHandler(Photo photo);
 public class PhotoProcessor
 {
     public void Process(Photo photo,PhotoFilterHandler filterHandler)
     {
         filterHandler(photo);

         photo.Save();
     }
 }

在c#中,委托本身就是一个类,他不仅要求我们可以从外部访问,同时要求我们在声明的时候应该把他当作一个类型来进行处理而不是字段。所以,实际上,相片的委托处理类型与相片处理类型是在同一个级别上的。

委托实例其实就是一个方法指针 pointer,他指向的是被委托方法的内存地址,而被委托的方法签名需要与委托签名一样。

1.2 一切皆地址

image

变量是用来寻找数据的内存地址,而方法则是用来寻找算法的内存地址。

1.3 委托

image

委托必须与被委托的方法“类型兼容”。也就是说委托与被委托方法的返回类型必须一致,而方法签名,也就是参数类型也必须一样。

var photo = Photo.load("photo.jpg");
var processor = new PhotoProcessor();
var filters = new PhotoFilters();
PhotoProcessor.PhotoFilterHandler filterHandler = filters.ApplyBrightness;
processor.Process(photo, filterHandler);

可以看到命令行中果然对相片调用了亮度滤镜。而对图片处理系统来说,它根本就也不知道main方法加载了什么滤镜。于是,通过委托的方法,我们很轻松就对图片处理系统和滤镜进行了姐耦。

使用接口配合多态,我们也同样能完成类似的任务,实现图片处理系统与滤镜系统的隔离。但是,从业务拓展的角度来说,使用委托可以带来更大的系统宽容度。

1.4 多播委托

image

委托的叠加使用叫做multicast,多播委托。使用 += 操作符,我们就可以进行委托的叠加操作了。比如说,加个对比度调节的滤镜,再加个大小剪切滤镜。而图片处理系统的代码我们完全不需要更改。

PhotoProcessor.PhotoFilterHandler filterHandler = filters.ApplyBrightness;
filterHandler += filters.ApplyContrast;
filterHandler += filters.Resize;

image

委托的能力是非常强大的,它可以指定一组需要外部执行方法,然后通过引用方法内存地址执行这一组方法,让他们自己运行。这样的操作无疑将会极大的增加系统的灵活性和可拓展性。

注意这里,第一次用的“=”,是赋值的语法;第二次,用的是“+=”,是绑定的语法。如果第一次就使用“+=”,将出现“使用了未赋值的局部变量”的编译错误。

1.5 Delegate Class

https://docs.microsoft.com/en-us/dotnet/api/system.delegate?view=net-6.0
实际上,c#中的委托delegate并不是方法,而是一个类,class。这是微软MSDN官网对delegate给出的解释。

https://docs.microsoft.com/en-us/dotnet/api/system.multicastdelegate?view=net-6.0
而多播委托,multicastDelgate同样也是个class,他继承于delegate class,由system.delegate.combine 方法产生

随着程序的运行,我们也会看到filterhandler的method是动态变化的。也就是说委托的内存地址也是动态变化的。

所以,多播委托将会指向一组方法的内存地址而不是单一的方法内存

2. 预定义委托方法Func\Action

在c#中,我们还有两个可以配合泛型来使用的、预定义的委托类型。他们就是Func与Action。

2.1【action】

action是c#定义在system命名空间下的,可以看作是c#底层系统级别的组成部分。action有两种类型,一种是带泛型的,另一种是不带泛型的。我们来看看带泛型的吧。有没有被吓一跳? 泛型action居然有16个方法重载,也就是说它最多能同时支持16个不同类型的参数。所以,action 可以委托任何一个参数在16位以内的方法。
image

2.2【func】

除了 action, c#还有另一种预定义委托,就是func。先来看看定义吧。
image

同样,func也定义在system命名空间中;添加泛型括号以后,我们能看到,func委托与action一样,也同样支持方法重载。不过,不一样的是,func委托的参数中还有一个输出传参,result,而且,它也支持17个不同类型的参数。

  • func委托的方法又返回值
  • action委托的方法是没有返回值的。
  • 所以,这就是为什么func的泛型参数中有一个输出传参,result。这个result就是用来定义返回类型的。

如果我们需要被委托的方法有一个参数,那么我们就用1个泛型的func;如果需要被委托的方法有2个参数,那么我们就用2个泛型的func;被委托方法有三个参数,那么就用3个泛型func。以此类推,最多可以使用16个参数。而action也是一样的道理。

这个func和action搞这么多参数重载是要干什么呢?其实,通过16种方法重载,func和action就可以获得足够的灵活性来在各种各样的方法中实现委托,而我们也不需要使用delegate关键词来自定义委托了。

修改代码:

public void Process(Photo photo, Action<Photo> filterHandler)
{
    filterHandler(photo);
    photo.Save();
}
//main
Action<Photo> filterHandler = filters.ApplyBrightness;

3. c# 预定义事件

image

  • 事件拥有者是timer,也就是闹钟
  • 事件是elapsed,
  • 事件的订阅者,事件的响应对象,huihui
  • 而事件处理器就是huihui的成员对象AlarmEventHandler,闹钟处理
  • 事件订阅就是这个timer的 += 操作符
 public class Huihui
 {
     internal void AlarmEventHandler(object? sender, ElapsedEventArgs e)
     {
         Console.WriteLine("闹钟响了,我不管,继续睡");
     }
 }

 /// <summary>
 /// 室友
 /// </summary>
 public class RoomMate
 {
     public int RageValue {  get; set; }//怒气值
     internal void AlarmEventHandler(object? sender, ElapsedEventArgs e)//sender是事件发出者,拥有者alarm
     {
         RageValue += 25;
         if(RageValue >=100)
         {
             Console.WriteLine("受不了了");
             var alarm=(Timer)sender;
             alarm.Stop();
         }
         else
         {
             Console.WriteLine("闹钟响了,我也不管,继续睡");
         }
        
     }
 }
 class Program
 {
     static void Main(string[] args)
     {
         var hui=new Huihui();//事件响应者
         var roomMate=new RoomMate();

         Timer alarm =new Timer();//事件拥有者
         alarm.Interval = 1000;                   // hui.AlarmEventHandler是事件处理器
         alarm.Elapsed += hui.AlarmEventHandler;  //事件alarm.Elapsed  每隔一秒发生一次
         alarm.Elapsed += roomMate.AlarmEventHandler;
         //事件订阅: +=

         alarm.Start();
         Console.Read();
     }
 }

4. 自定义事件

image
c#中自定义事件有两种声明方式:

  1. 第一种方式是借助委托,从底层上配置一个完整的事件声明过程,英语叫做events in general;
  2. 而另一种则是通过event关键词来处理的简化方案,利用c#的语法糖,极大的减少了事件声明的模版代码和开发难度,并且把事件从类的角度转化为字段式声明,英语叫做filed-like。

4.1 案例

 public class Order
 {//订单
     public int Id { get; set; }
     public DateTime DatePlaced { get; set; }
     public float TotalPrice { get; set; }
 }

在主程序中用户创建了一张订单,然后订单处理系统就会处理这张订单,比如加载优惠券、计算运费、使用微信或者阿里支付等等。当订单处理完成、支付成功以后,订单处理系统需要给用户的邮箱或者手机发送订单信息。
image
之前使用接口的多态来实现了这个功能。尝试一下使用事件来处理订单的通知过程。
事件是基于委托的

那么,对于我们的订单处理事件需要传递两个参数,
第一个参数比较简单,用来传递数据处理的信息,是订单信息 order;
而第二个参数就比较复杂了,这里需要传递的是与事件本身相关的信息。比如说,订单处理成功还是失败、处理时间、处理的详细说明等等。

在c#行业中,我们在使用事件或者委托的时候有个命名规范,就是需要使用EventHandler作为后缀。c#行业对事件信息参数也有个命名规范,事件参数后缀需要以EventArgs结尾。

根据c#的命名规范,触发事件的处理方法一边都叫做On什么什么,比如说我们这里的OnOrderProcessed,process加上ed表示过去式,所以这里的意思就是“当订单处理结束的时候,短信服务将会做什么什么事情

public class OrderProcessorEventArgs : EventArgs
{
    public string Status { get; set; }
    public DateTime ProcessingTime { get; set; }
    public string Desctiption { get; set; }
}
public delegate void OrderProcessorEventHandler(Order order, OrderProcessorEventArgs args);

delegate委托所定义的实际上是一个类,是class,并不是字段。从理论上来说,EventHandler是事件的处理过程,而orderprocessor是事件的拥有者,他们实际上应该是同一个级别的。

使用委托类型 OrderProcessorEventHandler 来声明一个委托类型的字段。不过,这个字段我不希望外部能够访问到,所以在这里我们需要使用private OrderProcessorEventHandler

private OrderProcessorEventHandler _orderProcessorEventHandler;

这个字段就是用来保存或者说引用事件处理器的,请注意,这个字段的底层结构依然是个委托类型。有了这个委托字段以后,我们就可以声明事件了。

我们不希望对外暴露委托类型字段,但是我们希望事件却是可以从外部访问的。所以,这里使用访问秀师傅 public。声明事件使用event 关键词,而这个事件的约束就是OrderProcessorEventHandler 这个委托类型本身,最后事件的名称我们就叫做OrderProcessorEvent。

对于事件,我们还需要让它有能力绑定或者说订阅事件处理器。这个过程也有一个比较特殊的语法结构,它语法有点类似属性getter和setter的语言,不过,关键词我们使用的是add添加,和remove删除。

在事件的添加过程中,我们实际上调用的还是委托,也就是私有字段 this._orderProcessorEventHandler。

public event OrderProcessorEventHandler OrderProcessorEventHandler
{
    add
    {
        this._orderProcessorEventHandler += value;
    }
    remove
    {
        this._orderProcessorEventHandler -= value;
    }
}
  • 拥有者OrderProcessor
  • 事件 OrderProcessorEvent
  • 事件响应者 SmsMessageService
  • 事件处理器 SmsMessageService.OnOrderProcessd;
  • 事件订阅 +=
 static void Main(string[] args)
 {
     var order = new Order
     {
         Id = 123,
         DatePlaced = DateTime.Now,
         TotalPrice = 30f
     };
     OrderProcessor orderProcessor = new OrderProcessor();
     orderProcessor.OrderProcessorEventHandler += SmsMessageService.OnOrderProcessd;
     orderProcessor.Process(order);
 }

5.字段式事件声明

在process中触发的是事件,所以我们有没有可能在这里使用事件,也就是OrderProcessorEvent来代替委托,也就是私有的_orderProcessorEventHandler呢?

image
在OrderProcessor中,这段代码是订单处理事件的声明和处理过程。我们先把这整段都删掉。删掉私有成员 _orderProcessorEventHandler;然后,再删掉eventhandler的添加和删除的过程;最后,在OrderProcessorEvent的最后加上冒号。

委托声明删掉,更换类型为EventHandler,EventHandler 可以定义泛型,泛型就是事件的参数类型,也就是我们的OrderProcessorEventArgs。
image
当我们把其他代码删除以后,c#的语法糖自动会把事件转化为字段式定义,于是,原本无法直接调用的事件委托也转变为可以使用的事件字段了。

eventHandler所定义的方法签名的第一个参数类型为object,而不是我们自定义deleget所定义的order。所以,我们必须让短信服务和邮件服务的事件处理器与eventHandler的输出类型一致。

public class SmsMessageService
{
    internal static void OnOrderProcessed(object sender, OrderProcessorEventArgs args)
    {
        var order = (Order)sender;
        Console.WriteLine($"发送短信,订单 {order.Id} , 处理结果: {args.Status}, 处理时间: {args.ProcessingTime}");
    }
}


internal class MailService
{
    internal static void OnOrderProcessed(object sender, OrderProcessorEventArgs args)
    {
        var order = (Order)sender;
        Console.WriteLine($"发送邮件,订单 {order.Id} , 处理结果: {args.Status}, 处理时间: {args.ProcessingTime}");
    }
}

posted @ 2023-11-06 16:53  huihui不会写代码  阅读(27)  评论(0编辑  收藏  举报