[.net 面向對象編程基礎] (20) 委託html
上節在講到LINQ的匿名方法中說到了委託,不過比較簡單,沒了解清楚不要緊,這節中會詳細說明委託。c++
1. 什麼是委託?express
學習委託,我想說,學會了就感受簡單的不能再簡單了,沒學過或者不肯瞭解的人,看着就不知所措了,其實很簡單。編程
委託在.net面向對象編程和學習設計模式中很是重要,是學習.net面向對象編程必需要學會並掌握的。設計模式
委託從字面上理解,就是把作一些事情交給別人來幫忙完成。在C#中也能夠這樣理解,委託就是動態調用方法。這樣說明,就很好理解了。數組
平時咱們會遇到這樣的例子須要處理,好比有一個動物園(Zoo)(我仍是之前面的動物來講吧)裏面有狗(Dog)、雞(Chicken)、羊(Sheep)……,也許還會再進來一些新品種。參觀動物員的人想聽動物叫聲,那麼可讓管理員協助(動物只聽懂管理員的),這樣就是一個委託的例子。安全
在實現委託以前,咱們先看一下委託的定義:函數
委託是一個類,它定義了方法的類型,使得能夠將方法看成另外一個方法的參數來進行傳遞,這種將方法動態地賦給參數的作法,能夠避免在程序中大量使用If-Else(Switch)語句,同時使得程序具備更好的可擴展性。學習
委託(delegate),有些書上叫代理或表明,都是一個意思,爲了不了另外一個概念代理(Proxy)混淆,仍是叫委託更好一些。this
學過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
==============================================================================================