委托和事件


一、委托

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();

 +=委托的最后輸出結果是什么是?請看下面的截圖:

可以看到,調用頭部的委托導致了所有委托方法的執行。為委托+=增加方法讓我們看起來像是委托被修改了,其實它們並沒有被修改。事實上, 委托是不變的 。在給委托增加或移除方法時,實際發生的是創建了一個新的委托。

從上面的截圖中可以看出:多播委托的執行結果是把所有傳入的方法都執行一遍,而且是按照實例化時傳入方法的順序依次執行的。(上面傳入了兩次當前類型的DoNoThing方法,所以會執行兩邊。)

-= 為委托實例移除方法,從方法鏈的尾部開始匹配,遇到第一個完全吻合的,移除且只移除一個,沒有也不異常。

1 method -= new NoReturnNoPara(this.DoNothing);
2 method -= new NoReturnNoPara(DoNothingStatic);
3 method -= new NoReturnNoPara(Student.StudyAdvanced);
4 method -= new NoReturnNoPara(new Student().Study);
5 method.Invoke();

 移除委托的執行結果是什么呢?在上面添加委托的時候傳入了5個方法,移除委托的時候移除了4個方法,應該只會執行DoNothing()這一個方法,是這樣的嗎?看看下面的運行結果:

從截圖中可以看出,最后的結果和我們猜測的結果不同,除了執行DoNothing()方法以外,還執行了Study()方法,但是添加的時候我們只添加了一個Study()方法,而且后面又移除掉了,那為什么還會執行這個方法呢?原因是因為添加和移除時候不是同一個實例的Study()方法(添加和移除的時候都是new了一個新實例),所以移除的時候不會被移除掉。怎么證明上面的原因是否正確呢?請看下面的代碼:

Console.WriteLine("***多播委托添加方法***");
// 實例化一個Student對象
Student student = new Student();
// 實例化委托
NoReturnNoPara method = new NoReturnNoPara(DoNothing);
method += new NoReturnNoPara(this.DoNothing);
method += new NoReturnNoPara(DoNothingStatic);
method += new NoReturnNoPara(Student.StudyAdvanced);
method += new NoReturnNoPara(new Student().Study);
method += new NoReturnNoPara(student.Study);
method.Invoke();

Console.WriteLine("***下面是多播委托移除方法***");
//-= 為委托實例移除方法,從方法鏈的尾部開始匹配,遇到第一個完全吻合的,移除且只移除一個,沒有也不異常
method -= new NoReturnNoPara(this.DoNothing);
method -= new NoReturnNoPara(DoNothingStatic);
method -= new NoReturnNoPara(Student.StudyAdvanced);//不是同一個實例,所以是不同的方法
method -= new NoReturnNoPara(student.Study);
method.Invoke();

 查看運行結果:

從運行結果中可以看出上面的原因是正確的。

注意:多播委托不能異步調用(即調用BeginInvoke()),因為多播委托里面有很多方法,異步調用的時候不知道該怎樣執行,是把所有方法同步執行呢還是按照順序依次執行呢,BeginInvoke不知道該如何調用,所以多播委托不能直接調用BeginInvoke()方法。那如果我想使用該怎么辦呢?可以使用GetInvocationList()方法,F12查看GetInvocationList()方法的定義:

那么可以使用如下的代碼:

1 foreach(NoReturnNoPara item in method.GetInvocationList())
2 {
3       item.Invoke();
4 }

上面的多播委托例子中一直都是使用的沒有返回值的委托,如果是有返回值的委托,那么返回值是什么呢?請看下面的例子:

先定義幾個有返回值的方法:

 1 private int GetSomething()
 2 {
 3        return 1;
 4 }
 5 private int GetSomething2()
 6 {
 7        return 2;
 8 }
 9 private int GetSomething3()
10 {
11         return 3;
12 }

 實例化委托:

1 WithReturnNoPara methodWithReturn = new WithReturnNoPara(this.GetSomething);
2 methodWithReturn += new WithReturnNoPara(this.GetSomething2);
3 methodWithReturn += new WithReturnNoPara(this.GetSomething3);
4 int iResult = methodWithReturn.Invoke();
5 Console.WriteLine("返回值:"+iResult.ToString());

運行程序查看結果:

從截圖中可以看出:帶返回值的多播委托的結果是最后添加的方法的返回值。中間方法的返回值都會被丟棄。

總結:多播委托一般用來調用無返回值的方法,不用來調用有返回值的方法,因為有返回值的多播委托中間的結果都會被丟棄掉。

二、總結

我們可以對委托做如下的總結:

  1. 委托是不可變的。
  2. 使用+=或-=操作符時,實際上是創建了新的委托實例,並把它賦給當前的委托變量。
  3. 如果多播委托的返回類型不是void,那么調用者從最后一個被調用的方法來接收返回值,前面的方法仍然會被調用,但是其返回值就被棄用了。
  4. 所有的委托類型都派生於System.MulticastDelegate,而它又派生於System.Delegate。
  5. C#會把作用於委托的+、-、+=、-=操作編譯成使用System.Delegate的Combine和Remove兩個靜態方法。

二、事件

1、什么是事件

事件是帶event關鍵字的委托的實例。

2、如何聲明事件

1 // 聲明委托
2 public delegate void MiaoDelegate();
3 // 聲明事件
4 public event MiaoDelegate MiaoDelegateHandlerEvent;

3、委托和事件的區別和聯系

委托是一個類型,例如Student類。

事件是委托類型的一個實例,例如具體的一個學生。

4、為什么要是有事件

事件不能直接執行Invoke()方法,可以限制變量被外部調用或者直接賦值。

注意:即使是在子類中,事件也不能調用Invoke()方法。

三、委托和事件的應用

來看下面的一個例子:

有一個Cat類,里面有一個Miao()的方法,貓叫了一聲,然后觸發一系列的后續動作,通常的實現代碼如下:

public void Miao()
{
     Console.WriteLine("{0} Miao", this.GetType().Name);

      new Mouse().Run();
      new Baby().Cry();
      new Mother().Wispher();
      new Father().Roar();
      new Neighbor().Awake();
      new Stealer().Hide();
      new Dog().Wang();
}

調用Miao()方法:

1 // 實例化
2 Cat cat = new Cat();
3 cat.Miao();

 

上面的代碼可以實現上述的需求,但是這段代碼耦合性很強,因為是在Miao()方法里面直接調用別的實例的方法,以后無論是增加或者修改、調整方法的調用順序,都要修改Miao()方法,使得Miao()方法不穩定。

下面使用委托來優化上面的代碼:

 1 // 聲明委托
 2 public delegate void MiaoDelegate();
 3 public MiaoDelegate MiaoDelegateHandler;
 4 public void MiaoNew()
 5 {
 6      Console.WriteLine("{0} MiaoNew", this.GetType().Name);
 7      if (this.MiaoDelegateHandler != null)
 8      {
 9           this.MiaoDelegateHandler.Invoke();
10      }
11 }

 

調用:

 1 Cat cat = new Cat();
 2 // 多播委托
 3 cat.MiaoDelegateHandler += new MiaoDelegate(new Mouse().Run);
 4 cat.MiaoDelegateHandler += new MiaoDelegate(new Baby().Cry);
 5 cat.MiaoDelegateHandler += new MiaoDelegate(new Mother().Wispher);
 6 cat.MiaoDelegateHandler += new MiaoDelegate(new Brother().Turn);
 7 cat.MiaoDelegateHandler += new MiaoDelegate(new Father().Roar);
 8 cat.MiaoDelegateHandler += new MiaoDelegate(new Neighbor().Awake);
 9 cat.MiaoDelegateHandler += new MiaoDelegate(new Stealer().Hide);
10 cat.MiaoDelegateHandler += new MiaoDelegate(new Dog().Wang);
11 cat.MiaoNew();

 

上面的委托也可以改為事件實現:

 1 // 聲明事件
 2 public event MiaoDelegate MiaoDelegateHandlerEvent;
 3 public void MiaoNewEvent()
 4 {
 5      Console.WriteLine("{0} MiaoNewEvent", this.GetType().Name);
 6      if (this.MiaoDelegateHandlerEvent != null)
 7      {
 8           this.MiaoDelegateHandlerEvent.Invoke();
 9      }
10 }

 

調用:

 1 Cat cat = new Cat();
 2 cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Mouse().Run);
 3 cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Baby().Cry);
 4 cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Mother().Wispher);
 5 cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Brother().Turn);
 6 cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Father().Roar);
 7 cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Neighbor().Awake);
 8 cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Stealer().Hide);
 9 cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Dog().Wang);
10 cat.MiaoNewEvent();

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM