委托和事件

Posted .NET开发菜鸟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了委托和事件相关的知识,希望对你有一定的参考价值。

一、委托

1、什么是委托

委托是面向对象的、类型安全的,是引用类型。使用delegate关键字进行定义。委托的本质就是一个类,继承自System.MulticastDelegate,而它又派生自System.Delegate。里面内置了几个方法 ,可以在类的外面声明委托,也可以在类的内部声明委托。

对委托的使用:先定义,后声明和实例化委托,然后作为参数传递给方法。

1.1 定义委托

下面是几种委托定义的例子:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyDelegateDemo
 8 {
 9     // 也可以在类的外面定义委托
10     public delegate void NoReturnNoParaOutClass();
11 
12     public class MyDelegate
13     {
14         // 声明无参数无返回值的泛型委托
15         public delegate void NoReturnNoPara<T>(T t);
16         // 声明无参数无返回值的委托
17         public delegate void NoReturnNoPara();
18         // 声明有参数无返回值的委托
19         public delegate void NoReturnWithPara(int x, int y);
20         // 声明无参数有返回值的委托
21         public delegate int WithReturnNoPara();
22         // 声明有参数有返回值的委托
23         public delegate string WithReturnWithPara(out int x,ref int y);
24     }
25 }

1.2 声明并实例化委托

实例化委托时参数传递的是一个方法,方法的签名必须和委托的签名一样(即方法的返回值类型、参数列表的参数类型都必须和定义的委托一致)。

1 // 委托的实例化,DoNothing是一个方法
2 // NoReturnNoPara是定义的无参无返回值的委托,所以DoNothing方法也必须是无参无返回值的
3 NoReturnNoPara method = new NoReturnNoPara(DoNothing);

 DoNothing()方法定义如下:

1 private void DoNothing()
2 {
3     Console.WriteLine("This is DoNothing");
4 }

1.3 委托实例的调用

1 // 调用委托
2 method.Invoke();
3 // Invoke也可以去掉
4 method();

注意:委托的调用和直接执行方法的效果是一样的,例如:

1 // 调用委托
2 method.Invoke();
3 // Invoke也可以去掉
4 method();
5 // 直接执行方法
6 this.DoNothing();

 在控制台的Main()方法里面,结果如下:

从截图中能够看出:三种方式的输出结果都是一样的。

2、委托类型和委托实例

委托类型:定义了委托实例可以调用的那类方法,具体来说,委托类型定义了方法的返回类型和参数类型,下面的代码定义了一个委托类型:

// 规定了可以调用的方法的返回值类型是int,有一个类型为int的参数
delegate  int Transformer(int x);

 

委托实例:把方法赋值给委托变量的时候就创建了委托实例,例如下面的代码:

Transformer t =new Transformer(Square);

 

也可以简写为下面的形式:

Transformer t = Square;

 

Square是定义的一个方法,其方法定义如下:

int Square(int x)
{
    return  x*x;
}

委托的实例其实就是调用者的委托:调用者调用委托,然后委托调用目标方法,间接的把调用者和目标方法解耦合。

讲到这里可能有人会问:既然使用委托和直接调用方法的效果是一样的,那为什么还要使用委托呢,直接调用方法多么简单?下面先来看一个实际的例子。

先定义一个Student类,里面有一些属性和方法,Student类定义如下:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyDelegateDemo
 8 {
 9     public class Student
10     {
11         public int Id { get; set; }
12         public string Name { get; set; }
13         public int ClassId { get; set; }
14         public int Age { get; set; }
15 
16         public static void Show()
17         {
18             Console.WriteLine("123");
19         }
20     }
21 }

 然后使用集合初始化器的方式初始化一个List<Student>集合,填充一些测试数据:

  1 private List<Student> GetStudentList()
  2 {
  3             #region 初始化数据
  4             List<Student> studentList = new List<Student>()
  5             {
  6                 new Student()
  7                 {
  8                     Id=1,
  9                     Name="老K",
 10                     ClassId=2,
 11                     Age=35
 12                 },
 13                 new Student()
 14                 {
 15                     Id=1,
 16                     Name="hao",
 17                     ClassId=2,
 18                     Age=23
 19                 },
 20                  new Student()
 21                 {
 22                     Id=1,
 23                     Name="大水",
 24                     ClassId=2,
 25                     Age=27
 26                 },
 27                  new Student()
 28                 {
 29                     Id=1,
 30                     Name="半醉人间",
 31                     ClassId=2,
 32                     Age=26
 33                 },
 34                 new Student()
 35                 {
 36                     Id=1,
 37                     Name="风尘浪子",
 38                     ClassId=2,
 39                     Age=25
 40                 },
 41                 new Student()
 42                 {
 43                     Id=1,
 44                     Name="一大锅鱼",
 45                     ClassId=2,
 46                     Age=24
 47                 },
 48                 new Student()
 49                 {
 50                     Id=1,
 51                     Name="小白",
 52                     ClassId=2,
 53                     Age=21
 54                 },
 55                  new Student()
 56                 {
 57                     Id=1,
 58                     Name="yoyo",
 59                     ClassId=2,
 60                     Age=22
 61                 },
 62                  new Student()
 63                 {
 64                     Id=1,
 65                     Name="冰亮",
 66                     ClassId=2,
 67                     Age=34
 68                 },
 69                  new Student()
 70                 {
 71                     Id=1,
 72                     Name="",
 73                     ClassId=2,
 74                     Age=30
 75                 },
 76                 new Student()
 77                 {
 78                     Id=1,
 79                     Name="毕帆",
 80                     ClassId=2,
 81                     Age=30
 82                 },
 83                 new Student()
 84                 {
 85                     Id=1,
 86                     Name="一点半",
 87                     ClassId=2,
 88                     Age=30
 89                 },
 90                 new Student()
 91                 {
 92                     Id=1,
 93                     Name="小石头",
 94                     ClassId=2,
 95                     Age=28
 96                 },
 97                 new Student()
 98                 {
 99                     Id=1,
100                     Name="大海",
101                     ClassId=2,
102                     Age=30
103                 },
104                  new Student()
105                 {
106                     Id=3,
107                     Name="yoyo",
108                     ClassId=3,
109                     Age=30
110                 },
111                   new Student()
112                 {
113                     Id=4,
114                     Name="unknown",
115                     ClassId=4,
116                     Age=30
117                 }
118             };
119             #endregion
120             return studentList;
121 }

 现在有一个需求,找出List<Student>集合里面年龄大于25的学生信息,代码如下:

List<Student> studentList = this.GetStudentList();
//找出年龄大于25
List<Student> resultAge = new List<Student>();//准备容器
foreach (Student student in studentList)//遍历数据源
{
      if (student.Age > 25)//判断条件
      {
           resultAge.Add(student);//满足条件的放入容器
      }
}
Console.WriteLine($"结果一共有{resultAge.Count()}个");

 使用一个foreach循环很容易得到全部年纪大于25的学生,这时又提出了需求:找出name长度大于2的学生、找出Name长度大于2 而且年龄大于25 而且班级id是2的学生,代码如下:

 1 //找出Name长度大于2
 2 List<Student> resultName = new List<Student>();
 3 foreach (Student student in studentList)
 4 {
 5        if (student.Name.Length > 2)
 6        {
 7               resultName.Add(student);
 8        }
 9 }
10 Console.WriteLine($"结果一共有{resultName.Count()}个");
11 
12 //找出Name长度大于2 而且年龄大于25 而且班级id是2
13 List<Student> result = new List<Student>();
14 foreach (Student student in studentList)
15 {
16         if (student.Name.Length > 2 && student.Age > 25 && student.ClassId == 2)
17         {
18             result.Add(student);
19         }
20 }
21 Console.WriteLine($"结果一共有{result.Count()}个");

 观察上面的代码,你会发现里面有很多重复的代码:每次都要先准备一个查询结果集的集合,然后遍历数据源,判断条件,把满足条件的放入到集合中。可不可以把上面的代码进行优化呢?请看下面的代码:

 1 private List<Student> GetList(List<Student> source, int type)
 2 {
 3             List<Student> result = new List<Student>();
 4             foreach (Student student in source)
 5             {
 6                 switch(type)
 7                 {
 8                     case 1:
 9                         if (student.Age > 25)
10                         {
11                             result.Add(student);
12                         }
13                         break;
14                     case 2:
15                         if (student.Name.Length > 2)
16                         {
17                             result.Add(student);
18                         }
19                         break;
20                     case 3:
21                         if (student.Name.Length > 2 && student.Age > 25 && student.ClassId == 2)
22                         {
23                             result.Add(student);
24                         }
25                         break;
26                 }
27             }
28             return result;
29 }

 在上面这段代码中,每次根据不同的type类型执行不同的判断条件,这样看起来可以把一些重复的代码进行了重用,但是这样又会有其他的问题:所有的判断逻辑都写在了一起,如果又增加了一种type类型或者判断逻辑改变了,就要修改整个代码,违反了开闭原则。

仔细观察上面的这段代码:GetList()方法需要传入一个int类型的参数,根据不同的参数,执行对应的逻辑。那么可不可以直接传递逻辑进来呢?逻辑就是方法,也就是说能不能传递一个方法进来。可能有人会问题,方法都是进行调用啊,怎么能进行传递呢?答案是肯定的:那就是使用上面讲到的委托。

以查询年龄大于25的学生为例:逻辑就是判断学生的年龄是否大于25,返回一个bool值,如果大于25就添加到集合中,根据逻辑,可以得到下面的方法:

1 private bool Than(Student student)
2 {
3       return student.Age > 25;
4 }

根据这个方法的签名可以定义如下的委托: 

1 // 定义委托
2 public delegate bool ThanDelegate(Student student);

修改上面GetList的方法,把委托作为参数传递进来:

 1 private List<Student> GetListDelegate(List<Student> source, ThanDelegate method)
 2 {
 3        List<Student> result = new List<Student>();
 4        foreach (Student student in source)
 5        {
 6              // 调用委托
 7              if (method.Invoke(student))
 8              {
 9                  result.Add(student);
10               }
11         }
12         return result;
13 }

 实例化委托:

1 // 实例化委托
2 ThanDelegate method = new ThanDelegate(this.Than);
3 List<Student> resultDele = this.GetListDelegate(studentList, method);
4 Console.WriteLine($"结果一共有{resultDele.Count()}个");

 另外两个可以定义如下的方法:

 1 /// <summary>
 2 /// 查询Name长度大于2
 3 /// </summary>
 4 /// <param name="student"></param>
 5 /// <returns></returns>
 6 private bool LengthThan(Student student)
 7 {
 8       return student.Name.Length > 2;
 9 }
10 
11 /// <summary>
12 /// 查询Name长度大于2 而且年龄大于25 而且班级id是2
13 /// </summary>
14 /// <param name="student"></param>
15 /// <returns></returns>
16 private bool AllThan(Student student)
17 {
18       return student.Name.Length > 2 && student.Age > 25 && student.ClassId == 2;
19 }

 实例化委托如下:

1 //Name长度大于2
2 ThanDelegate nameMethod = new ThanDelegate(LengthThan);
3 List<Student> nameList= this.GetListDelegate(studentList, nameMethod);
4 Console.WriteLine($"Name长达大于2的结果一共有{nameList.Count()}个");
5 
6 //Name长度大于2 而且年龄大于25 而且班级id是2
7 ThanDelegate allMethod = new ThanDelegate(AllThan);
8 List<Student> allList = this.GetListDelegate(studentList, allMethod);
9 Console.WriteLine($"Name长度大于2 而且年龄大于25 而且班级id是2的结果一共有{nameList.Count()}个");

观察GetListDelegate这个方法:保留了以前公用的代码:准备一个结果集的集合、循环遍历数据源,把符合条件的学生添加到结果集中,而判断逻辑放到了单独的一个方法中,如果判断逻辑改变了或者需要增加新的判断逻辑,只需要修改原有的判断逻辑或者新增判断逻辑即可,这样可以做到不需要修改GetListDelegate()这个方法,很好的符合开不原则。

可以总结出委托的一个应用:委托可以解除公用逻辑(准备结果集的集合、循环遍历数据源,添加到结果集中)和具体的业务逻辑(例如判断年龄大于25)的耦合,可以减少重复的代码。

2、多种途径实例化委托

委托实例化的时候不仅可以传入当前类型的普通方法,还可以传入静态、实例方法等,例如:

1 // 传入当前类型的普通方法
2 NoReturnNoPara method = new NoReturnNoPara(DoNothing);
3 // 传入当前类型的静态方法
4 NoReturnNoPara methodStatic = new NoReturnNoPara(DoNothingStatic);
5 // 传入其他类型的静态方法
6 NoReturnNoPara methodOtherStaitc = new NoReturnNoPara(Student.StudyAdvanced);
7 // 传入其他类型的普通方法
8 NoReturnNoPara methodOther = new NoReturnNoPara(new Student().Study);

 其中DoNothingStatic()方法定义如下:

1 private void DoNothingStatic()
2 {
3      Console.WriteLine("This is DoNothingStatic");
4 }

 Student类的静态方法和实例方法定义如下:

1 public static void StudyAdvanced()
2 {
3       Console.WriteLine("欢迎学习高级班课程");
4 }
5 
6 public void Study()
7 {
8       Console.WriteLine("学习");
9 }

总结:实例化委托时传入的方法只有一个要求:方法的签名和委托的签名一样,即返回值类型和参数列表一致,无论该方法来自于当前类型的普通方法、静态方法或者其他类型的实例方法和静态方法。

3、链式委托

链式委托也被称为“多播委托”,其本质是 一个由多个委托组成的链表 。我们知道,所有的自定义委托都继承自System.MulticastDelegate类,这个类就是为链式委托而设计的。当两个及以上的委托被链接到一个委托链时,调用头部的委托将导致该链上的所有委托方法都被执行

像上面实例化委托的时候,一个委托类型的变量只能保存一个方法,使用多播委托,一个委托类型的变量可以保存多个方法,多播委托可以增加、减少委托,Invoke的时候可以按顺序执行。

+= 为委托实例按顺序增加方法,形成方法链,Invoke时,按顺序依次执行,例如下面的代码:

1 // 实例化委托
2 NoReturnNoPara method = new NoReturnNoPara(DoNothing);
3 method += new NoReturnNoPara(this.DoNothing);
4 method += new NoReturnNoPara(DoNothingStatic);
5 method += new NoReturnNoPara(Student.StudyAdvanced);
6 method += new NoReturnNoPara(new Student().Study);
7 method.Invoke();

 +=委托的最后输出结果是什么是?请看下面的截图:

可以看到,调用头部的委托导致了所有委托方法的执行。为委托+=增加方法让我们看起来像是委托被修改了,其实它们并没有被修改。事实上, 委托是不变的 。在给委托增加或移除方法时,实际发生的是创建了一个新的委托。

从上面的截图中可以看出:多播委托的执行结果是把所有传入的方法都执行一遍,而

以上是关于委托和事件的主要内容,如果未能解决你的问题,请参考以下文章

原生js如何绑定a连接点击事件?

编写高质量代码改善C#程序的157个建议——建议137:委托和事件类型应添加上级后缀

jQuery事件绑定和委托实例

C#事件

概念篇-委托和事件

C#系列你应该知道的委托和事件