C#中的委托与事件笔记——进阶

一、委托基础
  1. 委托类型
    • 在 C# 里能写出的所有委托基本上都是多播委托(MulticastDelegate),常规方式无法写出普通的委托(Delegate),底层实现中才可能出现 Delegate 类型。
    • 多播委托可以添加多个方法,使用 += 运算符添加方法,其内部的 invocationList 用于存储方法。

      当委托为普通委托时,invocationList 指向一个方法;为多播委托时,它是一个数组,可存储多个方法。

    • 可通过 GetInvocationList 方法获取 invocationList 中的所有委托,该方法返回一个 Delegate 数组。
  2. 委托特性及问题
    委托内部存储了一个方法列表(数组),因此委托存在一些问题:
    • 调用报错中断:调用委托时,如果其中一个委托报错,后面的委托不会被调用。

    • 返回值问题:如果委托有返回值,注册多个方法时,调用委托(Invoke)的返回值会是最后一个方法的返回值。

    • 移除方法时间复杂度:使用 -= 运算符移除方法时,时间复杂度是 O(n)。

      从委托方法列表中Remove一个方法时,对该方法的查找是从后往前进行的

      若移除最早添加的方法,查找时间会显著增加。

    • 线程不安全:对委托数组的添加或删除操作没有提供线程锁,线程不安全。

      事件(对委托的一种封装)是线程安全的

  3. 委托与函数指针的区别
    • 委托可以指向多个函数:委托可以指向多个函数,而函数指针做不到。
    • 委托可以指向同一函数多次:委托可以指向同一个函数多次,在调用时会按顺序多次调用该函数,函数指针无法实现。
    • 包含对象信息:在 C# 中,将方法添加到委托时,方法所在类的实例信息也会作为信息传递过去,委托的函数引用包含了方法所在对象的信息;而 C++ 中的函数指针只是函数的入口地址,不具备这些信息。
  4. 委托的异步调用(几乎已弃用)
    • 委托提供了 BeginInvokeEndInvoke 方法用于异步调用,但在 .NET Core 及后续版本中不再使用。
    • 在旧版本中,BeginInvoke 会在另一个线程上运行任务,返回一个 IAsyncResult 类型,可通过其 IsCompleted 属性判断任务是否完成,使用 EndInvoke 结束调用并获取结果。现在通常使用 async/awaitTask 进行异步编程。

    在 UI 开发(如 WinForm、WPF)中,控件的 DispatcherBeginInvoke 方法,但一般采用开多线程或异步任务,完成任务后回到 UI 线程操作控件或调用 Dispatcher.Invoke

二、逆变与协变
  1. 概念
    • 协变(Covariance):把一个派生类转换为它的父类,通常可使用隐式转换。在委托中,若委托返回值类型为父类(如 object),可以添加返回值为派生类(如 string)的方法,这是允许的,即从派生类转为父类。
    • 逆变(Contravariance):把一个父类对象转换为它的派生类,通常使用显式转换。在委托中,若委托参数类型为派生类(如 string),可以添加参数类型为父类(如 object)的方法,相当于把父类逆变成派生类。

    .NET 中原生的强类型委托除了ActionFunc外,还包含一个Predicate(该委托并不常用),其返回值一定是布尔值,参数只能有一个int,常用于筛选,但在实际中,返回值为布尔值且参数为一个的lambda表达式通常会被转换为 Func 而非 Predicate

  2. 委托的逆变与协变:委托的逆变与协变指的是其传参以及返回值,泛型委托还提供了泛型逆变与协变,如 Action 中的 in 表示参数协变,IEnumerable 中的 out 表示参数逆变。
    在这里插入图片描述
三、委托与接口的比较

如果委托是为了实现将函数作为参数进行传参的话,为什么不使用接口呢?

使用接口的缺点:

  • 不灵活:使用接口传递不同形式参数时,需要定义多种接口,且必须有类去实现接口,繁琐且不实用。

    比如使用LINQ时,要频繁传入各种形式的函数

  • 暴露公共成员:传递接口对象实际上是传递类的实例引用,可能会暴露类中的其他公共成员,引发不必要麻烦。
  • 无法使用多播功能:接口无法像委托那样提供添加和删除等多播功能。
  • 方法封装固定:类的方法封装好后难以定制更改,而匿名委托(如lambda表达式)可随时声明,能使用当前作用域下的变量,提供高度灵活性。
  • 内置强类型委托丰富:C# 内置了很多强类型委托,如 ActionFuncPredicate 等,以及处理标准事件的 EventHandler,无需编写大量接口来满足不同参数和返回值需求。
四、事件
  1. 事件的本质
    • 事件使用 event 加上委托类型和事件实例名称声明,实际上是一个语法糖:
      1. 事件将委托以私有变量的形式封装在类内,外部无法直接访问和调用 Invoke 方法。
      2. 事件对委托进行了封装,定义了 addremove 两个方法,类似于属性的 getset,用于合并和移除委托。
      3. 事件提供了线程安全性,在addremove中使用 Interlocked.CompareExchange 进行线程锁操作。
  2. 自定义事件
    • 可以像自定义属性一样自定义事件的 addremove 方法。例如使用 List<Action> 存储委托,add 方法调用 ListAdd 方法添加委托,remove 方法调用 ListRemove 方法移除委托,还可提供公共方法调用委托列表中的每个委托。但自定义时要注意多线程安全性问题,若不提供线程安全措施,在多线程环境下频繁进行 addremove 操作可能会出现问题。

    • 在某些情况下,如 WPF 中使用 MVVM 模式实现 RelayCommand 类时,需要对事件的 addremove 方法进行定制化,可使用 CommandManager 的静态事件 RequerySuggested 对传入的函数进行注册或删除。也可以进行一些特殊定制,但不具有实际意义,只是为了展示其可行性。
      不使用自动属性

      运行结果为 3 method registered

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值