C#中的委托和事件
一、委托
1、什麼是委托
委托是面向對象的、類型安全的,是引用類型。使用delegate關鍵字進行定義。委托的本質就是一個類,繼承自System.MulticastDelegate,而它又派生自System.Delegate。裡面內置瞭幾個方法 ,可以在類的外面聲明委托,也可以在類的內部聲明委托。
對委托的使用:先定義,後聲明和實例化委托,然後作為參數傳遞給方法。
1.1 定義委托
下面是幾種委托定義的例子:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyDelegateDemo { // 也可以在類的外面定義委托 public delegate void NoReturnNoParaOutClass(); public class MyDelegate { // 聲明無參數無返回值的泛型委托 public delegate void NoReturnNoPara<T>(T t); // 聲明無參數無返回值的委托 public delegate void NoReturnNoPara(); // 聲明有參數無返回值的委托 public delegate void NoReturnWithPara(int x, int y); // 聲明無參數有返回值的委托 public delegate int WithReturnNoPara(); // 聲明有參數有返回值的委托 public delegate string WithReturnWithPara(out int x,ref int y); } }
1.2 聲明並實例化委托
實例化委托時參數傳遞的是一個方法,方法的簽名必須和委托的簽名一樣(即方法的返回值類型、參數列表的參數類型都必須和定義的委托一致)。
// 委托的實例化,DoNothing是一個方法 // NoReturnNoPara是定義的無參無返回值的委托,所以DoNothing方法也必須是無參無返回值的 NoReturnNoPara method = new NoReturnNoPara(DoNothing);
DoNothing()方法定義如下:
private void DoNothing() { Console.WriteLine("This is DoNothing"); }
1.3 委托實例的調用
// 調用委托 method.Invoke(); // Invoke也可以去掉 method();
註意:委托的調用和直接執行方法的效果是一樣的,例如:
// 調用委托 method.Invoke(); // Invoke也可以去掉 method(); // 直接執行方法 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類定義如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyDelegateDemo { public class Student { public int Id { get; set; } public string Name { get; set; } public int ClassId { get; set; } public int Age { get; set; } public static void Show() { Console.WriteLine("123"); } } }
然後使用集合初始化器的方式初始化一個List<Student>集合,填充一些測試數據:
private List<Student> GetStudentList() { #region 初始化數據 List<Student> studentList = new List<Student>() { new Student() { Id=1, Name="老K", ClassId=2, Age=35 }, new Student() { Id=1, Name="hao", ClassId=2, Age=23 }, new Student() { Id=1, Name="大水", ClassId=2, Age=27 }, new Student() { Id=1, Name="半醉人間", ClassId=2, Age=26 }, new Student() { Id=1, Name="風塵浪子", ClassId=2, Age=25 }, new Student() { Id=1, Name="一大鍋魚", ClassId=2, Age=24 }, new Student() { Id=1, Name="小白", ClassId=2, Age=21 }, new Student() { Id=1, Name="yoyo", ClassId=2, Age=22 }, new Student() { Id=1, Name="冰亮", ClassId=2, Age=34 }, new Student() { Id=1, Name="瀚", ClassId=2, Age=30 }, new Student() { Id=1, Name="畢帆", ClassId=2, Age=30 }, new Student() { Id=1, Name="一點半", ClassId=2, Age=30 }, new Student() { Id=1, Name="小石頭", ClassId=2, Age=28 }, new Student() { Id=1, Name="大海", ClassId=2, Age=30 }, new Student() { Id=3, Name="yoyo", ClassId=3, Age=30 }, new Student() { Id=4, Name="unknown", ClassId=4, Age=30 } }; #endregion return studentList; }
現在有一個需求,找出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的學生,代碼如下:
//找出Name長度大於2 List<Student> resultName = new List<Student>(); foreach (Student student in studentList) { if (student.Name.Length > 2) { resultName.Add(student); } } Console.WriteLine($"結果一共有{resultName.Count()}個"); //找出Name長度大於2 而且年齡大於25 而且班級id是2 List<Student> result = new List<Student>(); foreach (Student student in studentList) { if (student.Name.Length > 2 && student.Age > 25 && student.ClassId == 2) { result.Add(student); } } Console.WriteLine($"結果一共有{result.Count()}個");
觀察上面的代碼,你會發現裡面有很多重復的代碼:每次都要先準備一個查詢結果集的集合,然後遍歷數據源,判斷條件,把滿足條件的放入到集合中。可不可以把上面的代碼進行優化呢?請看下面的代碼:
private List<Student> GetList(List<Student> source, int type) { List<Student> result = new List<Student>(); foreach (Student student in source) { switch(type) { case 1: if (student.Age > 25) { result.Add(student); } break; case 2: if (student.Name.Length > 2) { result.Add(student); } break; case 3: if (student.Name.Length > 2 && student.Age > 25 && student.ClassId == 2) { result.Add(student); } break; } } return result; }
在上面這段代碼中,每次根據不同的type類型執行不同的判斷條件,這樣看起來可以把一些重復的代碼進行瞭重用,但是這樣又會有其他的問題:所有的判斷邏輯都寫在瞭一起,如果又增加瞭一種type類型或者判斷邏輯改變瞭,就要修改整個代碼,違反瞭開閉原則。
仔細觀察上面的這段代碼:GetList()方法需要傳入一個int類型的參數,根據不同的參數,執行對應的邏輯。那麼可不可以直接傳遞邏輯進來呢?邏輯就是方法,也就是說能不能傳遞一個方法進來。可能有人會問題,方法都是進行調用啊,怎麼能進行傳遞呢?答案是肯定的:那就是使用上面講到的委托。
以查詢年齡大於25的學生為例:邏輯就是判斷學生的年齡是否大於25,返回一個bool值,如果大於25就添加到集合中,根據邏輯,可以得到下面的方法:
private bool Than(Student student) { return student.Age > 25; }
根據這個方法的簽名可以定義如下的委托:
// 定義委托 public delegate bool ThanDelegate(Student student);
修改上面GetList的方法,把委托作為參數傳遞進來:
private List<Student> GetListDelegate(List<Student> source, ThanDelegate method) { List<Student> result = new List<Student>(); foreach (Student student in source) { // 調用委托 if (method.Invoke(student)) { result.Add(student); } } return result; }
實例化委托:
// 實例化委托 ThanDelegate method = new ThanDelegate(this.Than); List<Student> resultDele = this.GetListDelegate(studentList, method); Console.WriteLine($"結果一共有{resultDele.Count()}個");
另外兩個可以定義如下的方法:
/// <summary> /// 查詢Name長度大於2 /// </summary> /// <param name="student"></param> /// <returns></returns> private bool LengthThan(Student student) { return student.Name.Length > 2; } /// <summary> /// 查詢Name長度大於2 而且年齡大於25 而且班級id是2 /// </summary> /// <param name="student"></param> /// <returns></returns> private bool AllThan(Student student) { return student.Name.Length > 2 && student.Age > 25 && student.ClassId == 2; }
實例化委托如下:
//Name長度大於2 ThanDelegate nameMethod = new ThanDelegate(LengthThan); List<Student> nameList= this.GetListDelegate(studentList, nameMethod); Console.WriteLine($"Name長達大於2的結果一共有{nameList.Count()}個"); //Name長度大於2 而且年齡大於25 而且班級id是2 ThanDelegate allMethod = new ThanDelegate(AllThan); List<Student> allList = this.GetListDelegate(studentList, allMethod); Console.WriteLine($"Name長度大於2 而且年齡大於25 而且班級id是2的結果一共有{nameList.Count()}個");
觀察GetListDelegate這個方法:保留瞭以前公用的代碼:準備一個結果集的集合、循環遍歷數據源,把符合條件的學生添加到結果集中,而判斷邏輯放到瞭單獨的一個方法中,如果判斷邏輯改變瞭或者需要增加新的判斷邏輯,隻需要修改原有的判斷邏輯或者新增判斷邏輯即可,這樣可以做到不需要修改GetListDelegate()這個方法,很好的符合開不原則。
可以總結出委托的一個應用:委托可以解除公用邏輯(準備結果集的集合、循環遍歷數據源,添加到結果集中)和具體的業務邏輯(例如判斷年齡大於25)的耦合,可以減少重復的代碼。
2、多種途徑實例化委托
委托實例化的時候不僅可以傳入當前類型的普通方法,還可以傳入靜態、實例方法等,例如:
// 傳入當前類型的普通方法 NoReturnNoPara method = new NoReturnNoPara(DoNothing); // 傳入當前類型的靜態方法 NoReturnNoPara methodStatic = new NoReturnNoPara(DoNothingStatic); // 傳入其他類型的靜態方法 NoReturnNoPara methodOtherStaitc = new NoReturnNoPara(Student.StudyAdvanced); // 傳入其他類型的普通方法 NoReturnNoPara methodOther = new NoReturnNoPara(new Student().Study);
其中DoNothingStatic()方法定義如下:
private void DoNothingStatic() { Console.WriteLine("This is DoNothingStatic"); }
Student類的靜態方法和實例方法定義如下:
public static void StudyAdvanced() { Console.WriteLine("歡迎學習高級班課程"); } public void Study() { Console.WriteLine("學習"); }
總結:實例化委托時傳入的方法隻有一個要求:方法的簽名和委托的簽名一樣,即返回值類型和參數列表一致,無論該方法來自於當前類型的普通方法、靜態方法或者其他類型的實例方法和靜態方法。
3、鏈式委托
鏈式委托也被稱為“多播委托”,其本質是一個由多個委托組成的鏈表
。我們知道,所有的自定義委托都繼承自System.MulticastDelegate類,這個類就是為鏈式委托而設計的。當兩個及以上的委托被鏈接到一個委托鏈時,調用頭部的委托將導致該鏈上的所有委托方法都被執行。
像上面實例化委托的時候,一個委托類型的變量隻能保存一個方法,使用多播委托,一個委托類型的變量可以保存多個方法,多播委托可以增加、減少委托,Invoke的時候可以按順序執行。
+= 為委托實例按順序增加方法,形成方法鏈,Invoke時,按順序依次執行,例如下面的代碼:
// 實例化委托 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.Invoke();
+=委托的最後輸出結果是什麼是?請看下面的截圖:
可以看到,調用頭部的委托導致瞭所有委托方法的執行。為委托+=增加方法讓我們看起來像是委托被修改瞭,其實它們並沒有被修改。事實上,委托是不變的。在給委托增加或移除方法時,實際發生的是創建瞭一個新的委托。
從上面的截圖中可以看出:多播委托的執行結果是把所有傳入的方法都執行一遍,而且是按照實例化時傳入方法的順序依次執行的。(上面傳入瞭兩次當前類型的DoNoThing方法,所以會執行兩邊。)
-= 為委托實例移除方法,從方法鏈的尾部開始匹配,遇到第一個完全吻合的,移除且隻移除一個,沒有也不異常。
method -= new NoReturnNoPara(this.DoNothing); method -= new NoReturnNoPara(DoNothingStatic); method -= new NoReturnNoPara(Student.StudyAdvanced); method -= new NoReturnNoPara(new Student().Study); 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()方法的定義:
那麼可以使用如下的代碼:
foreach(NoReturnNoPara item in method.GetInvocationList()) { item.Invoke(); }
上面的多播委托例子中一直都是使用的沒有返回值的委托,如果是有返回值的委托,那麼返回值是什麼呢?請看下面的例子:
先定義幾個有返回值的方法:
private int GetSomething() { return 1; } private int GetSomething2() { return 2; } private int GetSomething3() { return 3; }
實例化委托:
WithReturnNoPara methodWithReturn = new WithReturnNoPara(this.GetSomething); methodWithReturn += new WithReturnNoPara(this.GetSomething2); methodWithReturn += new WithReturnNoPara(this.GetSomething3); int iResult = methodWithReturn.Invoke(); Console.WriteLine("返回值:"+iResult.ToString());
運行程序查看結果:
從截圖中可以看出:帶返回值的多播委托的結果是最後添加的方法的返回值。中間方法的返回值都會被丟棄。
總結:多播委托一般用來調用無返回值的方法,不用來調用有返回值的方法,因為有返回值的多播委托中間的結果都會被丟棄掉。
總結
我們可以對委托做如下的總結:
- 委托是不可變的。
- 使用+=或-=操作符時,實際上是創建瞭新的委托實例,並把它賦給當前的委托變量。
- 如果多播委托的返回類型不是void,那麼調用者從最後一個被調用的方法來接收返回值,前面的方法仍然會被調用,但是其返回值就被棄用瞭。
- 所有的委托類型都派生於System.MulticastDelegate,而它又派生於System.Delegate。
- C#會把作用於委托的+、-、+=、-=操作編譯成使用System.Delegate的Combine和Remove兩個靜態方法。
二、事件
1、什麼是事件
事件是帶event關鍵字的委托的實例。
2、如何聲明事件
// 聲明委托 public delegate void MiaoDelegate(); // 聲明事件 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()方法:
// 實例化 Cat cat = new Cat(); cat.Miao();
上面的代碼可以實現上述的需求,但是這段代碼耦合性很強,因為是在Miao()方法裡面直接調用別的實例的方法,以後無論是增加或者修改、調整方法的調用順序,都要修改Miao()方法,使得Miao()方法不穩定。
下面使用委托來優化上面的代碼:
// 聲明委托 public delegate void MiaoDelegate(); public MiaoDelegate MiaoDelegateHandler; public void MiaoNew() { Console.WriteLine("{0} MiaoNew", this.GetType().Name); if (this.MiaoDelegateHandler != null) { this.MiaoDelegateHandler.Invoke(); } }
調用:
Cat cat = new Cat(); // 多播委托 cat.MiaoDelegateHandler += new MiaoDelegate(new Mouse().Run); cat.MiaoDelegateHandler += new MiaoDelegate(new Baby().Cry); cat.MiaoDelegateHandler += new MiaoDelegate(new Mother().Wispher); cat.MiaoDelegateHandler += new MiaoDelegate(new Brother().Turn); cat.MiaoDelegateHandler += new MiaoDelegate(new Father().Roar); cat.MiaoDelegateHandler += new MiaoDelegate(new Neighbor().Awake); cat.MiaoDelegateHandler += new MiaoDelegate(new Stealer().Hide); cat.MiaoDelegateHandler += new MiaoDelegate(new Dog().Wang); cat.MiaoNew();
上面的委托也可以改為事件實現:
// 聲明事件 public event MiaoDelegate MiaoDelegateHandlerEvent; public void MiaoNewEvent() { Console.WriteLine("{0} MiaoNewEvent", this.GetType().Name); if (this.MiaoDelegateHandlerEvent != null) { this.MiaoDelegateHandlerEvent.Invoke(); } }
調用:
Cat cat = new Cat(); cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Mouse().Run); cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Baby().Cry); cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Mother().Wispher); cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Brother().Turn); cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Father().Roar); cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Neighbor().Awake); cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Stealer().Hide); cat.MiaoDelegateHandlerEvent += new MiaoDelegate(new Dog().Wang); cat.MiaoNewEvent();
到此這篇關於C#委托和事件的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。