[.net 面向對象編程基礎] (20) 委托
上節在講到LINQ的匿名方法中說到了委托,不過比較簡單,沒了解清楚沒關系,這節中會詳細說明委托。
1. 什么是委托?
學習委托,我想說,學會了就感覺簡單的不能再簡單了,沒學過或者不願了解的人,看着就不知所措了,其實很簡單。
委托在.net面向對象編程和學習設計模式中非常重要,是學習.net面向對象編程必須要學會並掌握的。
委托從字面上理解,就是把做一些事情交給別人來幫忙完成。在C#中也可以這樣理解,委托就是動態調用方法。這樣說明,就很好理解了。
平時我們會遇到這樣的例子需要處理,比如有一個動物園(Zoo)(我還是以前面的動物來說吧)里面有狗(Dog)、雞(Chicken)、羊(Sheep)……,也許還會再進來一些新品種。參觀動物員的人想聽動物叫聲,那么可以讓管理員協助(動物只聽懂管理員的),這樣就是一個委托的例子。
在實現委托之前,我們先看一下委托的定義:
委托是一個類,它定義了方法的類型,使得可以將方法當作另一個方法的參數來進行傳遞,這種將方法動態地賦給參數的做法,可以避免在程序中大量使用If-Else(Switch)語句,同時使得程序具有更好的可擴展性。
委托(delegate),有些書上叫代理或代表,都是一個意思,為了避免了另一個概念代理(Proxy)混淆,還是叫委托更好一些。
學過c++的人很熟悉指針,C#中沒有了指針,使用了委托,不同的是,委托是一個安全的類型,也是面向對象的。
2. 委托的使用
委托(delegate)的聲明的語法如下:
public delegate void Del(string parameter);
定義委托基本上是定義一個新類,所以可以在定義類的任何地方定義委托,既可以在另一個類的內部定義,也可以在任何類的外部定義,還可以在命名空間中把委托定義為頂層對象。根據定義的可見性,可以在委托定義上添加一般的訪問修飾符:public、private、protected等:
實際上,“定義一個委托”是指“定義一個新類”。只是把class換成了delegate而已,委托實現為派生自基類System. Multicast Delegate的類,System.MulticastDelegate又派生自基類System.Delegate。
下面我們使用委托來實現上面動物園的實例,實現如下:
1 /// <summary> 2 /// 動物類 3 /// </summary> 4 class Zoo 5 { 6 public class Manage 7 { 8 public delegate void Shout(); 9 public static void CallAnimalShout(Shout shout) 10 { 11 shout(); 12 } 13 } 14 public class Dog 15 { 16 string name; 17 public Dog(string name) 18 { 19 this.name = name; 20 } 21 public void DogShout() { 22 23 Console.WriteLine("我是小狗:" + this.name + "汪~汪~汪"); 24 } 25 } 26 public class Sheep 27 { 28 string name; 29 public Sheep(string name) 30 { 31 this.name = name; 32 } 33 public void SheepShout() 34 { 35 Console.WriteLine("我是小羊:" + this.name + "咩~咩~咩"); 36 } 37 } 38 public class Checken 39 { 40 string name; 41 public Checken(string name) 42 { 43 this.name = name; 44 } 45 public void ChickenShout() 46 { 47 Console.WriteLine("我是小雞:" + this.name + "喔~喔~喔"); 48 } 49 } 50 }
動物園除了各種動物外,還有動物管理員,動物管理員有一個委托。調用如下:
//參觀者委托管理員,讓某種動物叫 Zoo.Dog dog=new Zoo.Dog("汪財"); Zoo.Manage.Shout shout = new Zoo.Manage.Shout(dog.DogShout); //管理員收到委托傳達給動物,動物執行主人命令 Zoo.Manage.CallAnimalShout(shout);
運行結果如下:
上面的實例實現了委托的定義和調用,即間接的調用了動物叫的方法。肯定有人會說,為什么不直接調用小狗叫的方法,而要繞一大圈來使用委托。如果只是簡單的讓一種動物叫一下,那么用委托確實是繞了一大圈,但是如果我讓讓狗叫完,再讓羊叫,再讓雞叫,反反復復要了好幾種動物的叫聲,最后到如果要結算費用,誰能知道我消費了多少呢?如果一次讓幾種動物同時叫呢,我們是不是要再寫一個多個動物叫的方法來調用呢?當遇到復雜的調用時委托的作用就體現出來了,下面我們先看一下,如何讓多個動物同時叫,就是下面要說的多播委托。
委托需要滿足4個條件:
a.聲明一個委托類型
b.找到一個跟委托類型具有相同簽名的方法(可以是實例方法,也可以是靜態方法)
c.通過相同簽名的方法來創建一個委托實例
c.通過委托實例的調用完成對方法的調用
3. 多播委托
每個委托都只包含一個方法調用,調用委托的次數與調用方法的次數相同。如果調用多個方法,就需要多次顯示調用這個委托。當然委托也可以包含多個方法,這種委托稱為多播委托。
當調用多播委托時,它連續調用每個方法。在調用過程中,委托必須為同類型,返回類型一般為void,這樣才能將委托的單個實例合並為一個多播委托。如果委托具有返回值和/或輸出參數,它將返回最后調用的方法的返回值和參數。
下面我們看一下,調用“狗,雞,羊”同時叫的實現:
//聲明委托類型 Zoo.Manage.Shout shout; //加入狗叫委托 shout = new Zoo.Manage.Shout(new Zoo.Dog("小哈").DogShout); //加入雞叫委托 shout += new Zoo.Manage.Shout(new Zoo.Checken("大鵬").ChickenShout); //加入羊叫委托 shout += new Zoo.Manage.Shout(new Zoo.Sheep("三鹿").SheepShout); //執行委托 Zoo.Manage.CallAnimalShout(shout); Console.ReadLine();
運行結果如下:
上面的示例 ,多播委托用+=來添加委托,同樣可以使用 -=來移除委托。
上面的示例,如果我們感覺還不足以體現委托的作用。我們假動物除了會叫之外,還有其它特技。狗會表演“撿東西(PickUp)”,羊會踢球(PlayBall),雞會跳舞(Dance)
觀眾想看一個集體表演了,讓狗叫1次,搶一個東西回來;羊叫1次踢1次球,雞叫1次跳1只舞。 然后,順序倒過來再表演一次。如果使用直接調用方法,那么寫代碼要瘋了,順序執行一次,就順序寫一排方法代碼,要反過來表演,又要倒過來寫一排方法。這還不算高難度的表演,假如要穿插進行呢?使用委托的面向對象特征,我們實現這些需求很簡單。看代碼:
首先我們改進一下羊,狗,雞,讓他們有一個特技的方法。
1 /// <summary> 2 /// 動物類 3 /// </summary> 4 class Zoo 5 { 6 public class Manage 7 { 8 public delegate void del(); 9 10 /// <summary> 11 /// 動物表演 12 /// </summary> 13 /// <param name="obj"></param> 14 /// <param name="shout"></param> 15 public static void CallAnimal(del d) 16 { 17 d(); 18 } 19 } 20 public class Dog 21 { 22 string name; 23 public Dog(string name) 24 { 25 this.name = name; 26 } 27 public void DogShout() 28 { 29 Console.WriteLine("我是小狗:"+this.name+"汪~汪~汪"); 30 } 31 public void PickUp() 32 { 33 Console.WriteLine("小狗" + this.name + " 撿東西 回來了"); 34 } 35 } 36 public class Sheep 37 { 38 string name; 39 public Sheep(string name) 40 { 41 this.name = name; 42 } 43 public void SheepShout() 44 { 45 Console.WriteLine( "我是小羊:"+this.name+" 咩~咩~咩 "); 46 } 47 public void PlayBall() 48 { 49 Console.WriteLine("小羊" + this.name + " 打球 結束了"); 50 } 51 } 52 53 public class Chicken 54 { 55 string name; 56 public Chicken(string name) 57 { 58 this.name = name; 59 } 60 public void ChickenShout() 61 { 62 Console.WriteLine("我是小雞:"+this.name+"喔~喔~喔"); 63 } 64 public void Dance() 65 { 66 Console.WriteLine("小雞" + this.name + " 跳舞 完畢"); 67 } 68 } 69 }
調用如下:
1 //多播委托(二)動物狂歡 2 3 //挑選三個表演的動物 4 Zoo.Dog dog = new Zoo.Dog("小哈"); 5 Zoo.Chicken chicken = new Zoo.Chicken("大鵬"); 6 Zoo.Sheep sheep = new Zoo.Sheep("三鹿"); 7 8 //加入狗叫委托 9 Zoo.Manage.del dogShout = dog.DogShout; 10 //加入雞叫委托 11 Zoo.Manage.del chickenShout = chicken.ChickenShout; 12 //加入羊叫委托 13 Zoo.Manage.del sheepnShout = sheep.SheepShout; 14 15 //加入狗表演 16 Zoo.Manage.del dogShow = new Zoo.Manage.del(dog.PickUp); 17 //加入雞表演 18 Zoo.Manage.del chickenShow = new Zoo.Manage.del(chicken.Dance); 19 //加入羊表演 20 Zoo.Manage.del sheepShow = new Zoo.Manage.del(sheep.PlayBall); 21 22 23 //構造表演模式 24 //第一種表演方式:狗叫1次搶一個東西回來;羊叫1次踢1次球;雞叫1次跳1只舞; 25 Zoo.Manage.del del = dogShout + dogShow + chickenShout + chickenShow + sheepnShout + sheepShow; 26 //執行委托 27 Zoo.Manage.CallAnimal(del); 28 29 30 Console.WriteLine("\n第二種表演,順序反轉\n"); 31 //第二種表演,順序反轉 32 var del2 = del.GetInvocationList().Reverse(); 33 //執行委托 34 foreach (Zoo.Manage.del d in del2) 35 Zoo.Manage.CallAnimal(d); 36 Console.ReadLine();
運行結果如下:
使用多播委托有兩點要注意的地方:
(1)多播委托的方法並沒有明確定義其順序,盡量避免在對方法順序特別依賴的時候使用。
(2)多播委托在調用過程中,其中一個方法拋出異常,則整個委托停止。
4. 匿名方法
我們通常都都顯式定義了一個方法,以便委托調用,有一種特殊的方法,可以直接定義在委托實例的區塊里面。我們在LINQ基礎一節中,已經舉例說明過匿名方法。實例化普通方法的委托和匿名方法的委托有一點差別。下面我們看一下示例:
//定義委托 delegate void Add(int a,int b);
//實例委托,使用匿名方法 Add add = delegate(int a, int b) { Console.WriteLine(a + "+" + b + "=" + (a + b)); }; //調用 add(1, 2); add(11, 32);
返回結果為: 1+2=3 11+32=43
4.1 對於匿名方法有幾點注意:
(1)在匿名方法中不能使用跳轉語句調到該匿名方法的外部;反之亦然:匿名方法外部的跳轉語句不能調到該匿名方法的內部。
(2)在匿名方法內部不能訪問不完全的代碼。
(3)不能訪問在匿名方法外部使用的ref和out參數,但可以使用在匿名方法外部定義的其他變量。
(4)如果需要用匿名方法多次編寫同一個功能,就不要使用匿名方法,而編寫一個指定的方法比較好,因為該方法只能編寫一次,以后可通過名稱引用它。
4.2 匿名方法的適用環境:
(1)在調用上下文中的變量時
(2)該方法只調用一次時,如果方法在外部需要多次調用,建議使用顯示定義一個方法.
可見,匿名方法是一個輕量級的寫法。
4.3 使用Labmda表達式書寫匿名方法
在Linq基礎一節中,我們說了,Labmda表達式是基於數學中的λ(希臘第11個字母)演算得名,而“Lambda 表達式”(lambda expression)是指用一種簡單的方法書寫匿名方法。
上面的匿名方法,我們可以使用等效的Labmda表達式來書寫,如下:
//使用Lambda表達式的匿名方法 實例化並調用委托 Add add2 = (a, b) => { Console.WriteLine(a + "+" + b + "=" + (a + b)); }; add2(3, 4); add2(3, 31); //返回結果為:3+4=7 3+31=34
“=>”符號左邊為表達式的參數列表,右邊則是表達式體(body)。參數列表可以包含0到多個參數,參數之間使用逗號分割。
5. 泛型委托
前面我們說了通常情況下委托的聲明及使用,除此之外,還有泛型委托
泛型委托一共有三種:
Action(無返回值泛型委托)
Func(有返回值泛型委托)
predicate(返回值為bool型的泛型委托)
下面一一舉例說明
5.1 Action(無返回值泛型委托)
示例如下:
1 /// <summary> 2 /// 提供委托簽名方法 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 /// <param name="action"></param> 6 /// <param name="a"></param> 7 /// <param name="b"></param> 8 static void ActionAdd<T>(Action<T,T> action,T a,T b) 9 { 10 action(a,b); 11 } 12 13 //兩個被調用方法 14 static void Add(int a,int b) 15 { 16 Console.WriteLine(a + "+" + b + "=" + (a + b)); 17 } 18 19 static void Add(int a, int b,int c) 20 { 21 Console.WriteLine(a + "+" + b + "+"+c+"=" + (a + b)); 22 }
聲明及調用如下:
//普通方式調用 ActionAdd<int>(Add,1,2); //匿名方法聲明及調用 Action<int,int> acc = delegate(int a,int b){ Console.WriteLine(a + "+" + b + "=" + (a + b)); }; acc(11, 22); //表達式聲明及調用 Action<int, int> ac = (a,b)=>{ Console.WriteLine(a + "+" + b + "=" + (a + b)); }; ac(111, 222);
返回值如下:
可以使用 Action<T1, T2, T3, T4> 委托以參數形式傳遞方法,而不用顯式聲明自定義的委托。
封裝的方法必須與此委托定義的方法簽名相對應。 也就是說,封裝的方法必須具有四個均通過值傳遞給它的參數,並且不能返回值。
(在 C# 中,該方法必須返回 void)通常,這種方法用於執行某個操作。
5.2 Func(有返回值泛型委托)
示例如下:
1 /// <summary> 2 /// 提供委托簽名方法 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 /// <param name="action"></param> 6 /// <param name="a"></param> 7 /// <param name="b"></param> 8 static string FuncAdd<T,T2>(Func<T,T2,string> func,T a,T2 b) 9 { 10 return func(a,b); 11 } 12 13 //兩個被調用方法 14 static string Add(int a,int b) 15 { 16 return (a + "+" + b + "=" + (a + b)); 17 }
調用如下:
//有返回值的泛型委托Func //普通方式調用 Console.WriteLine(FuncAdd<int,int>(Add, 1, 2)); //匿名方法聲明及調用 Func<int,int,string> acc = delegate(int a,int b){ return (a + "+" + b + "=" + (a + b)); }; Console.WriteLine(acc(11, 22)); //表達式聲明及調用 Func<int, int,string> ac = (a, b) => {return (a + "+" + b + "=" + (a + b)); }; Console.WriteLine(ac(111, 222));
運行結果同上例
5.3 predicate(返回值為bool型的泛型委托)
表示定義一組條件並確定指定對象是否符合這些條件的方法。此委托由 Array 和 List 類的幾種方法使用,用於在集合中搜索元素。
使用MSDN官方的示例如下 :
1 //以下示例需要引用System.Drawing程序集 2 private static bool ProductGT10( System.Drawing.Point p) 3 { 4 if (p.X * p.Y > 100000) 5 { 6 return true; 7 } 8 else 9 { 10 return false; 11 } 12 }
調用及運行結果如下:
System.Drawing.Point[] points = { new System.Drawing.Point(100, 200), new System.Drawing.Point(150, 250), new System.Drawing.Point(250, 375), new System.Drawing.Point(275, 395), new System.Drawing.Point(295, 450) }; System.Drawing.Point first = Array.Find(points, ProductGT10); Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y); Console.ReadKey(); //輸出結果為: //Found: X = 275, Y = 395
6.委托中的協變和逆變
將方法簽名與委托類型匹配時,協變和逆變為您提供了一定程度的靈活性。協變允許方法具有的派生返回類型比委托中定義的更多。逆變允許方法具有的派生參數類型比委托類型中的更少
關於協變和逆變要從面向對象繼承說起。繼承關系是指子類和父類之間的關系;子類從父類繼承所以子類的實例也就是父類的實例。比如說Animal是父類,Dog是從Animal繼承的子類;如果一個對象的類型是Dog,那么他必然是Animal。
協變逆變正是利用繼承關系 對不同參數類型或返回值類型 的委托或者泛型接口之間做轉變。我承認這句話很繞,如果你也覺得繞不妨往下看看。
如果一個方法要接受Dog參數,那么另一個接受Animal參數的方法肯定也可以接受這個方法的參數,這是Animal向Dog方向的轉變是逆變。如果一個方法要求的返回值是Animal,那么返回Dog的方法肯定是可以滿足其返回值要求的,這是Dog向Animal方向的轉變是協變。
由子類向父類方向轉變是協變 協變用於返回值類型用out關鍵字
由父類向子類方向轉變是逆變 逆變用於方法的參數類型用in關鍵字
協變逆變中的協逆是相對於繼承關系的繼承鏈方向而言的。
6.1 數組的協變:
Animal[] animalArray = new Dog[]{};
上面一行代碼是合法的,聲明的數組數據類型是Animal,而實際上賦值時給的是Dog數組;每一個Dog對象都可以安全的轉變為Animal。Dog向Animal方法轉變是沿着繼承鏈向上轉變的所以是協變
6.2 委托中的協變和逆變
6.2.1 委托中的協變
//委托定義的返回值是Animal類型是父類 public delegate Animal GetAnimal(); //委托方法實現中的返回值是Dog,是子類 static Dog GetDog(){return new Dog();} //GetDog的返回值是Dog, Dog是Animal的子類;返回一個Dog肯定就相當於返回了一個Animal;所以下面對委托的賦值是有效的 GetAnimal getMethod = GetDog;
6.2.2 委托中的逆變
//委托中的定義參數類型是Dog public delegate void FeedDog(Dog target); //實際方法中的參數類型是Animal static void FeedAnimal(Animal target){} // FeedAnimal是FeedDog委托的有效方法,因為委托接受的參數類型是Dog;而FeedAnimal接受的參數是animal,Dog是可以隱式轉變成Animal的,所以委托可以安全的的做類型轉換,正確的執行委托方法; FeedDog feedDogMethod = FeedAnimal;
定義委托時的參數是子類,實際上委托方法的參數是更寬泛的父類Animal,是父類向子類方向轉變,是逆變
6.3 泛型委托的協變和逆變:
6.3.1 泛型委托中的逆變
如下委托聲明:
public delegate void Feed<in T>(T target)
Feed委托接受一個泛型類型T,注意在泛型的尖括號中有一個in關鍵字,這個關鍵字的作用是告訴編譯器在對委托賦值時類型T可能要做逆變
/先聲明一個T為Animal的委托 Feed<Animal> feedAnimalMethod = a=>Console.WriteLine(“Feed animal lambda”); //將T為Animal的委托賦值給T為Dog的委托變量,這是合法的,因為在定義泛型委托時有in關鍵字,如果把in關鍵字去掉,編譯器會認為不合法 Feed<Dog> feedDogMethod = feedAnimalMethod;
6.3.2 泛型委托中的協變
如下委托聲明:
public delegate T Find<out T>();
Find委托要返回一個泛型類型T的實例,在泛型的尖括號中有一個out關鍵字,該關鍵字表明T類型是可能要做協變的
//聲明Find<Dog>委托 Find<Dog> findDog = ()=>new Dog(); //聲明Find<Animal>委托,並將findDog賦值給findAnimal是合法的,類型T從Dog向Animal轉變是協變 Find<Animal> findAnimal = findDog;
6.4 泛型接口中的協變和逆變:
泛型接口中的協變逆變和泛型委托中的非常類似,只是將泛型定義的尖括號部分換到了接口的定義上。
6.4.1 泛型接口中的逆變
如下接口定義:
public interface IFeedable<in T> { void Feed(T t); }
接口的泛型T之前有一個in關鍵字,來表明這個泛型接口可能要做逆變
如下泛型類型FeedImp<T>,實現上面的泛型接口;需要注意的是協變和逆變關鍵字in,out是不能在泛型類中使用的,編譯器不允許
public class FeedImp<T>:IFeedable<T> { public void Feed(T t){ Console.WriteLine(“Feed Animal”); } }
來看一個使用接口逆變的例子:
IFeedable<Dog> feedDog = new FeedImp<Animal>();
上面的代碼將FeedImp<Animal>類型賦值給了IFeedable<Dog>的變量;Animal向Dog轉變了,所以是逆變
6.4.2 泛型接口中的協變
如下接口的定義:
public interface IFinder<out T> { T Find(); }
泛型接口的泛型T之前用了out關鍵字來說明此接口是可能要做協變的;如下泛型接口實現類
public class Finder<T>:IFinder<T> where T:new() { public T Find(){ return new T(); } }
//使用協變,IFinder的泛型類型是Animal,但是由於有out關鍵字,我可以將Finder<Dog>賦值給它
Finder<Animal> finder = new Finder<Dog>();
協變和逆變的概念不太容易理解,可以通過實際代碼思考理解。這么繞的東西到底有用嗎?答案是肯定的,通過協變和逆變可以更好的復用代碼。復用是軟件開發的一個永恆的追求。
7. 要點
7.1 委托的返回值及參數總結
(1)Delegate至少0個參數,至多32個參數,可以無返回值,也可以指定返回值類型
(2)Func可以接受0個至16個傳入參數,必須具有返回值
(3)Action可以接受0個至16個傳入參數,無返回值
(4)Predicate只能接受一個傳入參數,返回值為bool類型
7.2 委托的幾種寫法總結:
(1)、委托 委托名=new 委托(會調用的方法名); 委托名(參數);
(2)、委托 委托名 =會調用的方法名; 委托名(參數);
(3)、匿名方法
委托 委托名=delegate(參數){會調用的方法體};委托名(參數);
(4)、拉姆達表達式
委托 委托名=((參數1,。。參數n)=>{會調用的方法體});委托名(參數);
(5)、用Action<T>和Func<T>,第一個無返回值
Func<參數1, 參數2, 返回值> 委托名= ((參數1,參數2) => {帶返回值的方法體 });返回值=委托名(參數1,參數2);
7.3.重要的事情說三遍:
(1)“委托”(delegate)(代表、代理):是類型安全的並且完全面向對象的。在C#中,所有的代理都是從System.Delegate類派生的(delegate是System.Delegate的別名)。
(2)委托隱含具有sealed屬性,即不能用來派生新的類型。
(3)委托最大的作用就是為類的事件綁定事件處理程序。
(4)在通過委托調用函數前,必須先檢查委托是否為空(null),若非空,才能調用函數。
(5)委托理實例中可以封裝靜態的方法也可以封裝實例方法。
(6)在創建委托實例時,需要傳遞將要映射的方法或其他委托實例以指明委托將要封裝的函數原型(.NET中稱為方法簽名:signature)。注意,如果映射的是靜態方法,傳遞的參數應該是類名.方法名,如果映射的是實例方法,傳遞的參數應該是實例名.方法名。
(7)只有當兩個委托實例所映射的方法以及該方法所屬的對象都相同時,才認為它們是相等的(從函數地址考慮)。
(8)多個委托實例可以形成一個委托鏈,System.Delegate中定義了用來維護委托鏈的靜態方法Combion,Remove,分別向委托鏈中添加委托實例和刪除委托實例。
(9)委托三步曲:
a.生成自定義委托類:delegate int MyDelegate();
b.然后實例化委托類:MyDelegate d = new MyDelegate(MyClass.MyMethod);
c.最后通過實例對象調用方法:int ret = d()
(10)委托的返回值通常是void,雖然不是必須的,但是委托允許定義多個委托方法(即多播委托),設想他們都有返回值,最后返回的值會覆蓋前面的,因此通常都定義為void.
==============================================================================================
返回目錄
<如果對你有幫助,記得點一下推薦哦,有不明白的地方或寫的不對的地方,請多交流>
QQ群:467189533
==============================================================================================