C#委托

委托和事件,是将设计模式的一种结合到了语法当中,初学由于缺乏对设计模式和应用场景的认知,所以很容易卡在这块内容上。要理解这两个概念之前,首先是要理解他们的应用场景。首先说委托的用法,这将有助于我们理解“事件”。

1. 什么是委托

1.1 概述

委托的本质就是函数指针,存放函数地址的容器(将函数包裹一层),可以将函数地址当作一般的参数在另一个函数中传递,并可以在该函数体内调用。

函数指针: 函数指针是函数的内存地址, 本质是一个地址变量, 该地址指向一个函数。

2. 为什么要设计委托

在面向对象语言中,类和对象是对现实事物的抽象,类的三种主要成员分别是:

  • 负责保存数据的字段,描述了类或对象的状态(以及用于保护字段的属性);
  • 用于实现逻辑的方法,决定了类或对象可以做什么;
  • 用于通知其它类或对象来进行协调与信息交换的事件(事件是一种对委托的封装,后面会介绍)。

在这三种主要成员的作用下,面向对象编程得以在虚拟世界中对现实世界发生的种种逻辑进行模拟。通常来说,方法本身已经足以胜任“实现逻辑”了;为什么还需要委托呢?

第一个原因: 可以优化逻辑,在一个复杂的算法中,方法之间存在大量互相调用的需求。
此时我们可以把符合某种特征的方法给抽象出来,用委托封装,当作参数传递,允许逻辑共享, 减少重复代码。

第二个原因: 是解耦,增加灵活性, 委托在某种程度的提供了一个方法层面的间接层。

在代码设计中,有这样一句话“计算机科学中的每个问题都可以用一间接层解决”,间接层最主要思想是解耦和分治。

在面向对象语言中,相比面向过程,它使用对象当间接层来完成对一个复杂逻辑的解耦,分治成一个个职责单一的小对象,然后这些对象相互交互,使得工作容错性、稳定性达到极致。

同理,使用委托, 相当于将一块方法复杂逻辑解耦,分治成两部分,一部分通用逻辑固定,写死, 将变化的逻辑使用委托进行封装,当作参数传递,实现了分离逻辑的耦合性,隔离变化, 使得程序更容易扩展。

3. 使用委托

为了让委托做某事, 必须满足四个条件:

  • 声明委托类型;
  • 为委托实例的操作找到一个恰当的方法;
  • 必须创建一个委托实例;
  • 必须调用委托实例

3.1 声明委托类型

委托类型实际上只是参数类型的一个列表以及一个返回值。 规定了类型的实例能表示的操作。
例如, 以下声明了一个委托类型:

修饰符 delegate 返回值类型 委托名 ( 参数列表 );

从上面的定义可以看出,委托的定义与方法的定义是相似的。例如定义一个不带参数的委托,代码如下。

public delegate void PrintString(string x)

上述代码指出, 如果要创建PrintString的一个实例, 需要只带一个参数(一个字符串), 而且这个方法要有一个void返回类型(什么都不返回)。

说明:混乱的根源: 容易产生歧义的“委托” 。 委托经常别人误解, 这是由于大家喜欢用委托这个词来描述委托类型和委托实例。 因为委托也是一个类, 所以这两者的区别相当于任何一个类型和该类型的实例的区别。 例如,string类型本身和一组特定的字符串肯定不同。

3.2 为委托实例的操作找到一个恰当的方法

委托所能够包裹的方法,必须具有和该委托类型相同的签名。
以下是为PrintString实例准备的几个方法签名:

//第一个方法完全符合,所以可以用它来创建一个委托实例。
void PrintNote(string x)
//第二个方法虽然也有一个参数,但是不是string类型,所以不兼容PrintString委托类型。
void PrintInt(int x)
//第三个方法第一个参数类型匹配,但是参数列表数量不匹配,所以也不兼容。
void PrintTowString(string x, string y)
//第四个虽然有正确的参数列表,但是返回类型不是void。
//如果委托类型有返回类型,方法的返回类型也应该与之匹配。
int GetStringLength(string x)

假设现在又一个针对兼容的签名(PrintNote)的方法体。接着 可以实例化委托。

3.3 创建委托实例

现在有了一个委托类型和一个正确签名的方法, 接着可以创建委托类型的一个实例,

//和正常类型实例化一样,委托也需要声明变量使用new 关键字实例委托对象。
PrintString print;
//注意:委托在实例化时必须带入方法的具体名称
print = new PrintString(this.PrintNote);

如果操作的是静态方法, 指定类型名称就可以。 如果委托封装的是实例方法, 就需要先创建该方法对象的实例。
这和普通调用方法是一样的,如果操作的方法和委托在同一个类,那么可以使用实例方法 this引用作为前缀。
同样,这些规则和你直接调用方法没什么不同。

3.4 调用委托实例

在实例化委托后即可调用委托,语法形式如下。

委托对象名(参数列表);

在这里,参数列表中传递的参数与委托定义的参数列表相同即可。

3.5 一个完整的例子

using System;
//声明委托类型
//注意: 委托是一个类,不是类和对象的成员,可以声明在类的外部。
public delegate void PrintString(string x);

public class Printer
{
     //声明兼容的实例方法
     public void PrintNote(string note)
     {
        Console.WriteLine($"Print{note}");
     }

}

class program
{ 
    public static void Main()
    {
        //实例化打印机
        Printer printer = new Printer();
        //声明委托变量
        PrintString printString;
        //创建委托实例
        printString = new PrintString(printer.PrintNote)//调用委托实例, 本质是调用挂载在委托实例上的PrintNote方法
        printString("不要学编程")        
    }   
}

4. 预定义委托

上述介绍了委托的定义和怎么委托封装方法, 但是又有了新问题,繁琐,开发中使用委托初衷是为了解耦,较少重复代码, 但是为了封装方法,还要先定义一个委托, 然后再声明, 最后实例化调用等等,这不是本末倒置了吗? 还没体会到委托的好处,代码先写了不少。所以,C#也考虑到了这个问题, 也为了以后在框架中更好的使用委托(Linq), 于是对委托类型统一,给开发者提供了预定义委托类型, 能够满足大部分应用场景,省掉了自己创建委托的时间。

Linq: 是Language Integrated Query的简称,它是微软在.net framework 3.5里面新加入的特性,用以简化查询查询操作。

4.1 Func

委托是对方法的封装, 其参数列表和返回值要与封装方法的签名匹配, C#提供封装有返回值方法的Func泛型委托。
不熟悉泛型的同学可以查阅C#泛型详解

Func<>:最多支持16个泛型参数,约定最后一个类型为返回值类型。搭配泛型,参数类型自由,满足大部分有返回值方法应用。

//2个参数,一个返回值
Func<int, int, int> int1 = GetInt;
//1个参数,一个返回值
Func<int, int> int2 = GetIntOne;
//16个参数,一个返回值。
//Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>

 static int GetInt(int x, int y)
 {
        return x + y;
 }

注意: 上文中 对int1 委托实例代码 Func<int, int, int> int1 = GetInt
本质上还是 Func<int, int, int> int1 = new Func<int, int, int>(GetInt)
能够省略new 运算符,是因为C#对创建委托实例的操作做的简化
在后续使用简化实例化委托语法的时不要忘了其本质还是new一个委托实例对方法进行封装,不是直接等于方法,这点要注意。

4.2 Action

C#提供用来封装无返回值方法的泛型委托。

Action<>:最多支持16个泛型参数,无返回值。其参数也是泛型,实现参数类型自由,满足大部分无返回值方法应用。

//2个参数,无返回值
Action<int, int> int3 = PrintInt;
//16个参数,无返回值。
//Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>

**4.3 Predicate **

同时,C#为了应对一些特殊要求的应用场景,也提供了一些具有约束的预定义委托Predicate(谓词委托) 。

Predicate<>:只支持一个泛型参数,固定返回bool类型。

谓词委托: 谓词是一种返回真或假的方法,过滤值列表非常有用。 谓词委托是对谓词的封装。

//判断单个参数是否满足条件
// => : lambda 表达式, 后面会配合linq介绍。
//上下文意思是: 为委托predicate封装一个方法,方法返回值为bool值 方法体为 { return a > int1(1, 2); }
Predicate<int> predicate = a => a > int1(1, 2);

以上3种预定义委托类型足以满足大部分应用场景,省掉了自己创建委托的时间。

5. 多播委托

C# 在”委托“ 上了付出了巨大的努力, 为了统一类型和便捷使用提供了批量的预定义委托类型,后续还简化了创建委托实例的操作。
老式的自定义委托就像铺路石, 他为后续的委托特性和语法的变革和优化铺平了道路, 让开发者使用舒服的同时还提供了很多有用的好处。其中多播委托就是其中之一。

5.1 多播委托的本质

上述已经说到委托类型是一个Delegate类,其多播委托(多路广播委托)MulticastDelegate 也是一个特殊类, 它的内部维护这一个
Delegate对象链表, 称为调用列表,由一个或多个委托元素组成。大多数实现 delegate 关键字的委托类型,都是从 MulticastDelegate 类派生。由于OOP的继承机制, 声明的委托都具有MulticastDelegate类型的一切行为, 那么就可以在我们声明的委托类型上实现多播。

public abstract class MulticastDelegate : Delegate

继承Object ➡ Delegate ➡ MulticastDelegate

Delegate对象链表: 链表是一种常见的基础数据结构, 在上下文中指的是用来存储委托对象的一个集合容器。

换言之,就是可以在一个委托实例上,封装多个参数列表和返回值相同的方法。所以多播委托也叫委托链,委托组合。

5.2 使用多播委托

多播委托的初始化可以像普通委托一样,传入一个签名相同的实例方法。同时,多播委托重载了 += 运算符和 -=运算符,用来向其调用列表中添加或者删除方法。调用多播委托时,方法将按照添加的顺序被依次调用。

         static void Main(string[] args)
         {
            //声明、调用方法与普通委托相同
            //多播委托封装多个方法也要遵守普通委托封装的原则, 委托链表中和封装的多个方法的签名必须匹配,一致。
            Action<int, int> action = null;
            
            //这句话实例一个委托对象 封装一个GetInt方法。
            //封装第一个方法不能用+=方式,因为action 开始为null,还没有在内存中开辟空间, 所以只能用=赋值。
             action = GetInt;
             
            //多播委托重载了 += 运算符, 其调用委托列表中添加委托对象
            //注意: 此时的添加委托对象只是C#简化实例化委托对象的操作,不是直接添加方法,等同于下面 new Action<int, int>(GetIntTow);
            action += GetIntTow;
            action += new Action<int, int>(GetIntTow);
            
            //多播委托重载了 -= 运算符, 其调用委托列表中删除委托对象
            action -= GetInt;

            //调用多播委托 本质上是按照添加顺序 依次调用封装的多个方法。
            //上述委托对象链表增加了一个GetInt()方法和 两个GetIntTow()方法,然后又删除了一个GetInt()。
            // 所以现在链表中只有两个GetIntTow()方法, 依次执行两个方法。
            action(1, 3);
        }
        
        //声明两个签名相同的方法
        static public void GetInt(int x, int y)  { return x + y; }
        
        static public void GetIntTow(int x, int y) {return x + y;}      

5.3 多播委托应用场景

认识完多播委托(MulticastDelegate) , 明白了它的本质就是一个特殊的委托类,在其内部封装和维护了一个委托组合,以及 使用重载的+= 和 -= 操作符去添加和删除委托。下面介绍多播委托最主要应用场景,事件

事件其实就是对委托的封装,或者说是对委托的约束, 因为委托太自由和灵活。

例如:实例一个委托添加一堆方法传递外部四处调用, 破坏封装,不能滥用; 又或者定义乱七八糟的委托类型扔在程序中,一个方法签名套一个委托类型,造成委托类型爆炸, 增加了代码复杂性,不利于维护。

所以C# 在一步一步对委托增加约束, 推出预定义委托类型潜方面限制了大量的自定义委托类型, 推出了事件机制对委托的安全性做了限制。接下来可以查看C#事件机制了解其具体对委托做了哪些约束和改进, 或者利用委托玩出了哪些花样。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值