天天看點

C#綜合揭秘——深入分析委托與事件

引言

本篇文章将為你介紹一下 Delegate 的使用方式,逐漸揭開 C# 當中事件(Event)的由來,它能使處理委托類型的過程變得更加簡單。

還将為您解釋委托的協變與逆變,以及如何使用 Delegate 使 Observer(觀察者)模式的使用變得更加簡單。

在事件的介紹上,會講述事件的使用方式,并以ASP.NET的使用者控件為例子,介紹一下自定義事件的使用。

最後一節,将介紹Predicate<T>、Action<T>、Func<T,TResult>多種泛型委托的使用和Lambda的發展過程與其使用方式。

因為時間倉促,文中有錯誤的地方敬請點評。

目錄

一、委托類型的來由

二、建立委托類

三、委托使用方式

四、深入解析事件

五、Lambda 表達式

記得在使用C語言的年代,整個項目中都充滿着針指的身影,那時候流行使用函數指針來建立回調函數,使用回調可以把函數回調給程式中的另一個函數。但函數指針隻是簡單地把位址指向另一個函數,并不能傳遞其他額外資訊。

在.NET中,在大部分時間裡都沒有指針的身影,因為指針被封閉在内部函數當中。可是回調函數卻依然存在,它是以委托的方式來完成的。委托可以被視為一個更進階的指針,它不僅僅能把位址指向另一個函數,而且還能傳遞參數,傳回值等多個資訊。系統還為委托對象自動生成了同步、異步的調用方式,開發人員使用 BeginInvoke、EndInvoke 方法就可以抛開 Thread 而直接使用多線程調用 。

回到目錄

使用delegate就可以直接建立任何名稱的委托類型,當進行系統編譯時,系統就會自動生成此類型。您可以使用delegate void MyDelegate() 方式建立一個委托類,并使用ILDASM.exe觀察其成員。由ILDASM.exe 中可以看到,它繼承了System.MulticastDelegate類,并自動生成BeginInvoke、EndInvoke、Invoke 等三個常用方法。

C#綜合揭秘——深入分析委托與事件

Invoke 方法是用于同步調用委托對象的對應方法,而BeginInvoke、EndInvoke是用于以異步方式調用對應方法的。

對于異步調用的使用方式,可以參考:C#綜合揭秘——細說多線程

1      public class MyDelegate:MulticastDelegate
2      {
3          //同步調用委托方法
4          public virtual void Invoke();
5          //異步調用委托方法
6          public virtual IAsyncResult BeginInvoke(AsyncCallback callback,object state);
7          public virtual void EndInvoke(IAsyncResult result);
8      }      

MulticastDelegate是System.Delegate的子類,它是一個特殊類,編譯器和其他工具可以從此類派生,但是自定義類不能顯式地從此類進行派生。它支援多路廣播委托,并擁有一個帶有連結的委托清單,在調用多路廣播委托時,系統将按照調用清單中的委托出現順序來同步調用這些委托。

MulticastDelegate具有兩個常用屬性:Method、Target。其中Method 用于擷取委托所表示的方法Target 用于擷取目前調用的類執行個體。

MulticastDelegate有以下幾個常用方法:

方法名稱 說明
 Clone   建立委托的淺表副本。
 GetInvocationList   按照調用順序傳回此多路廣播委托的調用清單。
 GetMethodImpl   傳回由目前的 MulticastDelegate 表示的靜态方法。
 GetObjectData   用序列化該執行個體所需的所有資料填充 SerializationInfo 對象。
 MemberwiseClone   建立目前 Object 的淺表副本。
 RemoveImpl   調用清單中移除與指定委托相等的元素

MulticastDelegate與Delegate給委托對象建立了強大的支援,下面向各位詳細介紹一下委托的使用方式。

3.1 簡單的委托

當建立委托對象時,委托的參數類型必須與委托方法相對應。隻要向建立委托對象的構造函數中輸入方法名稱example.Method,委托就會直接綁定此方法。使用myDelegate.Invoke(string message),就能顯式調用委托方法。但在實際的操作中,我們無須用到 Invoke 方法,而隻要直接使用myDelegate(string message),就能調用委托方法。

1     class Program
 2     {
 3         delegate void MyDelegate(string message);
 4 
 5         public class Example
 6         {
 7             public void Method(string message)
 8             {
 9                 MessageBox.Show(message);
10             }
11         }
12 
13         static void Main(string[] args)
14         {
15             Example example=new Example();
16             MyDelegate myDelegate=new MyDelegate(example.Method);
17             myDelegate("Hello World");
18             Console.ReadKey();
19         }
20     }      

3.2 帶傳回值的委托

當建立委托對象時,委托的傳回值必須與委托方法相對應。使用下面的例子,方法将傳回 “Hello Leslie” 。

1     class Program
 2     {
 3         delegate string MyDelegate(string message);
 4 
 5         public class Example
 6         {
 7             public string Method(string name)
 8             {
 9                 return "Hello " + name;
10             }
11         }
12 
13         static void Main(string[] args)
14         {
15             Example example=new Example();
16             //綁定委托方法
17             MyDelegate myDelegate=new MyDelegate(example.Method);
18             //調用委托,擷取傳回值
19             string message = myDelegate("Leslie");
20             Console.WriteLine(message);
21             Console.ReadKey();
22         }
23     }      

3.3 多路廣播委托

在第二節前曾經提過,委托類繼承于MulticastDelegate,這使委托對象支援多路廣播,即委托對象可以綁定多個方法。當輸入參數後,每個方法會按順序進行疊代處理,并傳回最後一個方法的計算結果。

下面的例子中,Price 類中有兩個計算方法,Ordinary 按普通的9.5折計算,Favourable 按優惠價 8.5 折計算。委托同時綁定了這兩個方法,在輸入參數100以後,Ordinary、Favourable這兩個方法将按順序疊代執行下去,最後傳回 Favourable 方法的計算結果 85。

1         delegate double MyDelegate(double message);
 2 
 3         public class Price
 4         {
 5             public double Ordinary(double price)
 6             {
 7                 double price1 = 0.95 * price;
 8                 Console.WriteLine("Ordinary Price : "+price1);
 9                 return price1;
10             }
11 
12             public double Favourable(double price)
13             {
14                 double price1 = 0.85 * price;
15                 Console.WriteLine("Favourable Price : " + price1);
16                 return price1;
17             }
18 
19             static void Main(string[] args)
20             {
21                 Price price = new Price();
22                 //綁定Ordinary方法
23                 MyDelegate myDelegate = new MyDelegate(price.Ordinary);
24                 //綁定Favourable方法
25                 myDelegate += new MyDelegate(price.Favourable);
26                 //調用委托
27                 Console.WriteLine("Current Price : " + myDelegate(100));
28                 Console.ReadKey();
29             }
30         }      

運作結果

C#綜合揭秘——深入分析委托與事件

3.4 淺談Observer模式

回顧一下簡單的 Observer 模式,它使用一對多的方式,可以讓多個觀察者同時關注同一個事物,并作出不同的響應。

例如下面的例子,Manager的底薪為基本工資的1.5倍,Assistant的底薪為基本工資的1.2倍。WageManager類的RegisterWorker方法與RemoveWorker方法可以用于注冊和登出觀察者,最後執行Execute方法可以對多個已注冊的觀察者同時輸入參數。

C#綜合揭秘——深入分析委托與事件
1     public class WageManager
 2     {
 3         IList<Worker> workerList = new List<Worker>();
 4         
 5         public void RegisterWorker(Worker worker)
 6         {
 7             workerList.Add(worker);
 8         }
 9 
10         public void RemoveWorker(Worker worker)
11         {
12             workerList.Remove(worker);
13         }
14 
15         public void Excute(double basicWages)
16         {
17             if (workerList.Count != 0)
18                 foreach (var worker in workerList)
19                     worker.GetWages(basicWages);
20         }
21 
22         static void Main(string[] args)
23         {
24             WageManager wageManager = new WageManager();
25             //注冊觀察者
26             wageManager.RegisterWorker(new Manager());
27             wageManager.RegisterWorker(new Assistant());
28             //同時輸入底薪3000元,分别進行計算
29             wageManager.Excute(3000);
30 
31             Console.ReadKey();
32         }
33     }
34 
35     public abstract class Worker
36     {
37         public abstract double GetWages(double basicWages);
38     }
39 
40     public class Manager:Worker
41     {
42          //Manager實際工資為底薪1.5倍
43         public override double GetWages(double basicWages)
44         {
45             double totalWages = 1.5 * basicWages;
46             Console.WriteLine("Manager's wages is " + totalWages);
47             return totalWages;
48         }
49     }
50 
51     public class Assistant : Worker
52     {
53         //Assistant實際工資為底薪的1.2倍
54         public override double GetWages(double basicWages)
55         {
56             double totalWages = 1.2 * basicWages;
57             Console.WriteLine("Assistant's wages is " + totalWages);
58             return totalWages;
59         }
60     }      
C#綜合揭秘——深入分析委托與事件

開發 Observer 模式時借助委托,可以進一步簡化開發的過程。由于委托對象支援多路廣播,是以可以把Worker類省略。在WageManager類中建立了一個委托對象wageHandler,通過Attach與Detach方法可以分别加入或取消委托。如果觀察者想對事物進行監測,隻需要加入一個委托對象即可。記得在第二節曾經提過,委托的GetInvodationList方法能擷取多路廣播委托清單,在Execute方法中,就是通過去多路廣播委托清單去判斷所綁定的委托數量是否為0。

1         public delegate double Handler(double basicWages);
 2  
 3          public class Manager
 4          {
 5              public double GetWages(double basicWages)
 6              {
 7                  double totalWages=1.5 * basicWages;
 8                  Console.WriteLine("Manager's wages is : " + totalWages);
 9                  return totalWages;
10              }
11          }
12  
13          public class Assistant
14          {
15              public double GetWages(double basicWages)
16              {
17                  double totalWages = 1.2 * basicWages;
18                  Console.WriteLine("Assistant's wages is : " + totalWages);
19                  return totalWages;
20              }
21          }
22  
23          public class WageManager
24          {
25              private Handler wageHandler;
26  
27              //加入觀察者
28              public void Attach(Handler wageHandler1)
29              {
30                  wageHandler += wageHandler1;
31              }
32  
33              //删除觀察者
34              public void Detach(Handler wageHandler1)
35              {
36                  wageHandler -= wageHandler1;
37              }
38  
39              //通過GetInvodationList方法擷取多路廣播委托清單,如果觀察者數量大于0即執行方法
40              public void Execute(double basicWages)
41              {
42                  if (wageHandler!=null)
43                     if(wageHandler.GetInvocationList().Count() != 0)
44                         wageHandler(basicWages);
45              }
46  
47              static void Main(string[] args)
48              {
49                  WageManager wageManager = new WageManager();
50                  //加入Manager觀察者
51                  Manager manager = new Manager();
52                  Handler managerHandler = new Handler(manager.GetWages);
53                  wageManager.Attach(managerHandler);
54  
55                  //加入Assistant觀察者
56                  Assistant assistant = new Assistant();
57                  Handler assistantHandler = new Handler(assistant.GetWages);
58                  wageManager.Attach(assistantHandler);
59  
60                  //同時加入底薪3000元,分别進行計算
61                  wageManager.Execute(3000);
62                  Console.ReadKey();
63              }
64          }      

最後運作結果與上面的例子相同。

3.5 委托的協變與逆變

在 Framework 2.0 出現之前,委托協變這個概念還沒有出現。此時因為委托是安全類型,它們不遵守繼承的基礎規則。即會這下面的情況:Manager 雖然是 Worker 的子類,但 GetWorkerHander 委托不能直接綁定 GetManager 方法,因為在委托當中它們的傳回值 Manager 與 Worker 被視為完全無關的兩個類型。

1      public class Worker
 2      {.......}
 3      public class Manager:Worker
 4      {.......}
 5  
 6       class Program
 7      {
 8          public delegate Worker GetWorkerHandler(int id);
 9          public delegate Manager GetManagerHandler(int id);
10  
11          public static Worker GetWorker(int id)
12          {
13              Worker worker = new Worker();
14              ..............
15              return worker;
16          }
17  
18          public static Manager GetManager(int id)
19          {
20              Manager manager = new Manager();
21              ..............
22              return manager;
23          }
24  
25          static void Main(string[] args)
26          {
27              GetWorkerHandler workerHandler = new GetWorkerHandler(GetWorker);
28              var worker=workerHandler(1);
29  
30              GetManagerHandler managerHandler = new GetManagerHandler(GetManager);
31              var manager = managerHandler(2);
32              Console.ReadKey();
33          }
34      }      

自從Framework 2.0 面試以後,委托協變的概念就應運而生,此時委托可以按照傳統的繼承規則進行轉換。即 GetWorkerHandler 委托可以直接綁定 GetManager 方法。

1      public class Worker
 2      {.......}
 3      public class Manager:Worker
 4      {.......}
 5  
 6       class Program
 7      {
 8          public delegate Worker GetWorkerHandler(int id);
 9          //在 Framework2.0 以上,委托 GetWorkerHandler 可綁定 GetWorker 與 GetManager 兩個方法
10  
11          public static Worker GetWorker(int id)
12          {
13              Worker worker = new Worker();
14              return worker;
15          }
16  
17          public static Manager GetManager(int id)
18          {
19              Manager manager = new Manager();
20              return manager;
21          }
22  
23         static void Main(string[] args)
24         {
25             GetWorkerHandler workerHandler = new GetWorkerHandler(GetWorker);
26             Worker worker=workerHandler(1);
27             GetWorkerHandler managerHandler = new GetWorkerHandler(GetManager);
28             Manager manager = managerHandler(2) as Manager;
29             Console.ReadKey();
30         }
31      }      

委托逆變,是指委托方法的參數同樣可以接收 “繼承” 這個傳統規則。像下面的例子,以 object 為參數的委托,可以接受任何 object 子類的對象作為參數。最後可以在處理方法中使用 is 對輸入資料的類型進行判斷,分别處理對不同的類型的對象。

1     class Program
 2     {
 3         public delegate void Handler(object obj);
 4 
 5         public static void GetMessage(object message)
 6         {
 7             if (message is string)
 8                 Console.WriteLine("His name is : " + message.ToString());
 9             if (message is int)
10                 Console.WriteLine("His age is : " + message.ToString());
11         }
12 
13         static void Main(string[] args)
14         {
15             Handler handler = new Handler(GetMessage);
16             handler(29);
17             Console.ReadKey();
18         }
19    }      
C#綜合揭秘——深入分析委托與事件

注意:委托與其綁定方法的參數必須一至,即當 Handler 所輸入的參數為 A 類型,其綁定方法 GetMessage 的參數也必須為 A 類或者 A 的父類 。相反,當綁定方法的參數為 A 的子類,系統也無法辨認。

3.6 泛型委托

委托逆變雖然實用,但如果都以 object 作為參數,則需要每次都對參數進行類型的判斷,這不禁令人感到厭煩。

為此,泛型委托應運而生,泛型委托有着委托逆變的優點,同時利用泛型的特性,可以使一個委托綁定多個不同類型參數的方法,而且在方法中不需要使用 is 進行類型判斷,進而簡化了代碼。

1     class Program
 2     {
 3         public delegate void Handler<T>(T obj);
 4 
 5         public static void GetWorkerWages(Worker worker)
 6         {
 7             Console.WriteLine("Worker's total wages is " + worker.Wages);
 8         }
 9 
10         public static void GetManagerWages(Manager manager)
11         {
12             Console.WriteLine("Manager's total wages is "+manager.Wages);
13         }
14 
15         static void Main(string[] args)
16         {
17             Handler<Worker> workerHander = new Handler<Worker>(GetWorkerWages);
18             Worker worker = new Worker();
19             worker.Wages = 3000;
20             workerHander(worker);
21 
22             Handler<Manager> managerHandler = new Handler<Manager>(GetManagerWages);
23             Manager manager = new Manager();
24             manager.Wages = 4500;
25             managerHandler(manager);
26 
27             Console.ReadKey();
28         }
29     }      
C#綜合揭秘——深入分析委托與事件

4.1 事件的由來

在介紹事件之前大家可以先看看下面的例子, PriceManager 負責對商品價格進行處理,當委托對象 GetPriceHandler 的傳回值大于100元,按8.8折計算,低于100元按原價計算。

1     public delegate double PriceHandler();
 2 
 3     public class PriceManager
 4     {
 5         public PriceHandler GetPriceHandler;
 6 
 7         //委托處理,當價格高于100元按8.8折計算,其他按原價計算
 8         public double GetPrice()
 9         {
10             if (GetPriceHandler.GetInvocationList().Count() > 0)
11             {
12                 if (GetPriceHandler() > 100)
13                     return GetPriceHandler()*0.88;
14                 else
15                     return GetPriceHandler();
16             }
17             return -1;
18         }
19     }
20 
21     class Program
22     {
23         static void Main(string[] args)
24         {
25             PriceManager priceManager = new PriceManager();
26             
27             //調用priceManager的GetPrice方法擷取價格
28             //直接調用委托的Invoke擷取價格,兩者進行比較
29             priceManager.GetPriceHandler = new PriceHandler(ComputerPrice);
30             Console.WriteLine(string.Format("GetPrice\n  Computer's price is {0}!",
31                 priceManager.GetPrice()));
32             Console.WriteLine(string.Format("Invoke\n  Computer's price is {0}!",
33                 priceManager.GetPriceHandler.Invoke()));
34             
35             Console.WriteLine();
36             
37             priceManager.GetPriceHandler = new PriceHandler(BookPrice);
38             Console.WriteLine(string.Format("GetPrice\n  Book's price is {0}!",
39                 priceManager.GetPrice()));
40             Console.WriteLine(string.Format("Invoke\n  Book's price is {0}!" ,
41                 priceManager.GetPriceHandler.Invoke()));
42             
43             Console.ReadKey();
44         }
45         //書本價格為98元
46         public static double BookPrice()
47         {
48             return 98.0;
49         }
50         //計算機價格為8800元
51         public static double ComputerPrice()
52         {
53             return 8800.0;
54         }
55     }      
C#綜合揭秘——深入分析委托與事件

觀察運作的結果,如果把委托對象 GetPriceHandler 設定為 public ,外界可以直接調用 GetPriceHandler.Invoke 擷取運作結果而移除了 GetPrice 方法的處理,這正是開發人員最不想看到的。

為了保證系統的封裝性,開發往往需要把委托對象 GetPriceHandler 設定為 private, 再分别加入 AddHandler,RemoveHandler 方法對 GetPriceHandler 委托對象進行封裝。

1     public delegate double PriceHandler();
 2 
 3     public class PriceManager
 4     {
 5         private PriceHandler GetPriceHandler;
 6 
 7         //委托處理,當價格高于100元按8.8折計算,其他按原價計算
 8         public double GetPrice()
 9         {
10             if (GetPriceHandler!=null)
11             {
12                 if (GetPriceHandler() > 100)
13                     return GetPriceHandler()*0.88;
14                 else
15                     return GetPriceHandler();
16             }
17             return -1;
18         }
19 
20         public void AddHandler(PriceHandler handler)
21         {
22             GetPriceHandler += handler;
23         }
24 
25         public void RemoveHandler(PriceHandler handler)
26         {
27             GetPriceHandler -= handler;
28         }
29     }
30     ................
31     ................      

為了儲存封裝性,很多操作都需要加入AddHandler、RemoveHandler 這些相似的方法代碼,這未免令人感到厭煩。

為了進一步簡化操作,事件這個概念應運而生。

4.2 事件的定義

事件(event)可被視作為一種特别的委托,它為委托對象隐式地建立起add_XXX、remove_XXX 兩個方法,用作注冊與登出事件的處理方法。而且事件對應的變量成員将會被視為 private 變量,外界無法超越事件所在對象直接通路它們,這使事件具備良好的封裝性,而且免除了add_XXX、remove_XXX等繁瑣的代碼。

1     public class EventTest
2     {
3         public delegate void MyDelegate();
4         public event MyDelegate MyEvent;
5     }      

觀察事件的編譯過程可知,在編譯的時候,系統為 MyEvent 事件自動建立add_MyEvent、remove_MyEvent 方法。

C#綜合揭秘——深入分析委托與事件

4.3 事件的使用方式

事件能通過+=和-=兩個方式注冊或者登出對其處理的方法,使用+=與-=操作符的時候,系統會自動調用對應的 add_XXX、remove_XXX 進行處理。

值得留意,在PersonManager類的Execute方法中,如果 MyEvent 綁定的處理方法不為空,即可使用MyEvent(string)引發事件。但如果在外界的 main 方法中直接使用 personManager.MyEvent (string) 來引發事件,系統将引發錯誤報告。這正是因為事件具備了良好的封裝性,使外界不能超越事件所在的對象通路其變量成員。

注意:在事件所處的對象之外,事件隻能出現在+=,-=的左方。

 此時,開發人員無須手動添加 add_XXX、remove_XXX 的方法,就可實作與4.1例子中的相同功能,實作了良好的封裝。

1     public delegate void MyDelegate(string name);
 2 
 3     public class PersonManager
 4     {
 5         public event MyDelegate MyEvent;
 6 
 7         //執行事件
 8         public void Execute(string name)
 9         {
10             if (MyEvent != null)
11                 MyEvent(name);
12         }
13     }
14 
15     class Program
16     {
17         static void Main(string[] args)
18         {
19             PersonManager personManager = new PersonManager();
20             //綁定事件處理方法
21             personManager.MyEvent += new MyDelegate(GetName);
22             personManager.Execute("Leslie");
23             Console.ReadKey();
24         }
25 
26         public static void GetName(string name)
27         {
28             Console.WriteLine("My name is " + name);
29         }
30     }      

4.4 事件處理方法的綁定

在綁定事件處理方法的時候,事件出現在+=、-= 操作符的左邊,對應的委托對象出現在+=、-= 操作符的右邊。對應以上例子,事件提供了更簡單的綁定方式,隻需要在+=、-= 操作符的右方寫上方法名稱,系統就能自動辯認。

1     public delegate void MyDelegate(string name);
 2 
 3     public class PersonManager
 4     {
 5         public event MyDelegate MyEvent;
 6         .........
 7     }
 8 
 9     class Program
10     {
11         static void Main(string[] args)
12         {
13             PersonManager personManager = new PersonManager();
14             //綁定事件處理方法
15             personManager.MyEvent += GetName;
16             .............
17         }
18 
19         public static void GetName(string name)
20         {.........}
21    }      

如果覺得編寫 GetName 方法過于麻煩,你還可以使用匿名方法綁定事件的處理。

1     public delegate void MyDelegate(string name);
 2 
 3     public class PersonManager
 4     {
 5         public event MyDelegate MyEvent;
 6 
 7         //執行事件
 8         public void Execute(string name)
 9         {
10             if (MyEvent != null)
11                 MyEvent(name);
12         }
13 
14         static void Main(string[] args)
15         {
16             PersonManager personManager = new PersonManager();
17             //使用匿名方法綁定事件的處理
18             personManager.MyEvent += delegate(string name){
19                 Console.WriteLine("My name is "+name);
20             };
21             personManager.Execute("Leslie");
22             Console.ReadKey();
23         }
24     }      

4.5 C#控件中的事件

在C#控件中存在多個的事件,像Click、TextChanged、SelectIndexChanged 等等,很多都是通過 EventHandler 委托綁定事件的處理方法的,EventHandler 可說是C#控件中最常見的委托 。

public delegate void EventHandler (Object sender, EventArgs e)

EventHandler 委托并無傳回值,sender 代表引發事件的控件對象,e 代表由該事件生成的資料 。在ASP.NET中可以直接通過btn.Click+=new EventHandler(btn_onclick) 的方式為控件綁定處理方法。

1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head runat="server">
 3     <title></title>
 4     <script type="text/C#" runat="server">
 5         protected void Page_Load(object sender, EventArgs e)
 6         {
 7             btn.Click += new EventHandler(btn_onclick);
 8         }
 9 
10         public void btn_onclick(object obj, EventArgs e)
11         {
12             Button btn = (Button)obj;
13             Response.Write(btn.Text);
14         }
15     </script>
16 </head>
17 <body>
18     <form id="form1" runat="server">
19     <div>
20        <asp:Button ID="btn" runat="server" Text="Button"/>
21     </div>
22     </form>
23 </body>
24 </html>      

更多時候,隻需要在頁面使用 OnClick=“btn_onclick" 方法,在編譯的時候系統就會自動對事件處理方法進行綁定。

1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head runat="server">
 3     <title></title>
 4     <script type="text/C#" runat="server">
 5         public void btn_onclick(object obj, EventArgs e)
 6         {
 7             Button btn = (Button)obj;
 8             Response.Write(btn.Text);
 9         }
10     </script>
11 </head>
12 <body>
13     <form id="form1" runat="server">
14     <div>
15        <asp:Button ID="btn" runat="server" Text="Button" OnClick="btn_onclick"/>
16     </div>
17     </form>
18 </body>
19 </html>      

EventHandler 隻是 EventHandler<TEventArgs> 泛型委托的一個簡單例子。事實上,大家可以利用 EventHandler<TEventArgs> 構造出所需要的委托。

public delegate void EventHandler<TEventArgs> (Object sender, TEventArgs e)

在EventHandler<TEventArgs>中,sender代表事件源,e 代表派生自EventArgs類的事件參數。開發人員可以建立派生自EventArgs的類,從中加入需要使用到的事件參數,然後建立 EventHandler<TEventArgs> 委托。

下面的例子中,先建立一個派生自EventArgs的類MyEventArgs作為事件參數,然後在EventManager中建立事件myEvent , 通過 Execute 方法可以激發事件。最後在測試中綁定 myEvent 的處理方法 ShowMessage,在ShowMessage顯示myEventArgs 的事件參數 Message。

1     public class MyEventArgs : EventArgs
 2     {
 3         private string args;
 4 
 5         public MyEventArgs(string message)
 6         {
 7             args = message;
 8         }
 9 
10         public string Message
11         {
12             get { return args; }
13             set { args = value; }
14         }
15     }
16 
17     public class EventManager
18     {
19         public event EventHandler<MyEventArgs> myEvent;
20 
21         public void Execute(string message)
22         {
23             if (myEvent != null)
24                 myEvent(this, new MyEventArgs(message));
25         }
26     }
27 
28     class Program
29     {
30         static void Main(string[] args)
31         {
32             EventManager eventManager = new EventManager();
33             eventManager.myEvent += new EventHandler<MyEventArgs>(ShowMessage);
34             eventManager.Execute("How are you!");
35             Console.ReadKey();
36         }
37 
38         public static void ShowMessage(object obj,MyEventArgs e)
39         {
40             Console.WriteLine(e.Message);
41         }
42     }      
C#綜合揭秘——深入分析委托與事件

4.6 為使用者控件建立事件

在ASP.NET開發中,頁面往往會出現很多類似的控件與代碼,開發人員可以通過使用者控件來避免重複的代碼。但往往同一個使用者控件,在不同的頁面中需要有不同的響應。此時為使用者控件建立事件,便可輕松地解決此問題。

下面例子中,在使用者控件 MyControl 中建立存在一個GridView控件,GridView 控件通過 GetPersonList 方法擷取資料源。在使用者控件中還定義了 RowCommand 事件,在 GridView 的 GridView_RowCommand 方法中激發此事件。這樣,在頁面使用此控件時,開發人員就可以定義不同的方法處理 RowCommand 事件。

1 public class Person
 2 {
 3     public int ID
 4     { get; set; }
 5     public string Name
 6     { get; set; }
 7     public int Age
 8     { get; set; }
 9 }
10 
11 <!--    使用者控件     -->
12 <%@ Control Language="C#" AutoEventWireup="true" CodeFile="MyControl.ascx.cs" Inherits="MyControl" %>
13 <script type="text/C#" runat="server">
14     protected void Page_Load(object sender, EventArgs e)
15     {
16         GridView1.DataSource = GetPersonList();
17         GridView1.DataBind();
18     }
19 
20     //綁定資料源
21     protected IList<Person> GetPersonList()
22     {
23         IList<Person> list = new List<Person>();
24         Person person1 = new Person();
25         person1.ID = 1;
26         person1.Name = "Leslie";
27         person1.Age = 29;
28         list.Add(person1);
29         ...........
30         return list;
31     }
32 
33     public event GridViewCommandEventHandler RowCommand;
34 
35     protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
36     {
37         if (RowCommand != null)
38             RowCommand(sender, e);
39     }
40 </script>
41 <div>
42    <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" 
43         onrowcommand="GridView1_RowCommand">
44       <Columns>
45           <asp:BoundField DataField="ID" HeaderText="ID"/>
46           <asp:BoundField DataField="Name" HeaderText="Name"/>
47           <asp:BoundField DataField="Age" HeaderText="Age"/>
48           <asp:ButtonField CommandName="Get" Text="Select"/>
49       </Columns>
50    </asp:GridView>
51 </div>
52 
53 <!--     頁面代碼       -->
54 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs" Inherits="Default2" %>
55 <%@ Register Src="~/MyControl.ascx" TagPrefix="ascx" TagName="myControl" %>
56 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
57 
58 <html xmlns="http://www.w3.org/1999/xhtml">
59 <head runat="server">
60     <title></title>
61     <script type="text/C#" runat="server">
62        protected void myControl_RowCommand(object sender, GridViewCommandEventArgs e)
63        {
64           if (e.CommandName == "Get")
65           {
66             GridView gridView=(GridView)sender;
67             int index = int.Parse(e.CommandArgument.ToString());
68             label.Text=gridView.Rows[index].Cells[1].Text;
69           }
70        }
71    </script>
72 </head>
73 <body>
74     <form id="form1" runat="server">
75     <div>
76        <ascx:myControl ID="myControl" runat="server" OnRowCommand="myControl_RowCommand"></ascx:myControl>
77        <br />
78         Select Name : <asp:Label ID="label" runat="server"></asp:Label><br />
79     </div>
80     </form>
81 </body>
82 </html>      
C#綜合揭秘——深入分析委托與事件

使用控件已有的事件固然簡單,但它限制了傳送的參數類型,使開發人員無法傳送額外的自定義參數。在結構比較複雜的使用者控件中,使用已有的控件事件,顯然不夠友善,此時,您可以考慮為使用者控件建立自定義事件。

首先使用者控件中包含訂單資訊與訂單明細清單,首先定義一個事件參數 MyEventArgs,裡面包含了訂單資訊與一個 OrderItem 數組。然後建立使用者控件的委托MyDelegate 與對應的事件 MyEvent,在 Button 的 Click 事件中激發 MyEvent 自定義事件。這樣在頁面處理方法 myControl_Click 中就可以通過事件參數 MyEventArgs 擷取使用者控件中的屬性,計算訂單的總體價格。

1 <!--   基礎類    -->
  2  public class OrderItem
  3  {
  4      public OrderItem(string id,string goods,double price,int count)
  5      {
  6          this.OrderItemID = id;     //明細單ID
  7          this.Goods = goods;        //商品名稱
  8          this.Price = price;        //商品單價
  9          this.Count = count;        //商品數量 
 10      }
 11  
 12      public string OrderItemID
 13      { get; set; }
 14      public string Goods
 15      { get; set; }
 16      public double Price
 17      { get; set; }
 18      public int Count
 19      { get; set; }
 20  }
 21  
 22  /// 事件參數
 23  public class MyEventArgs:EventArgs
 24  {
 25      public MyEventArgs(string name,string address,string tel,
 26                         string orderCode,IList<OrderItem> orderItemList)
 27      {
 28          Name = name;    //買家姓名
 29          Address = address;    //買家位址
 30          Tel = tel;    //買家電話
 31          OrderCode = orderCode;     //訂單号碼
 32          OrderItemList = orderItemList;     //訂單明細
 33      }
 34  
 35      public string Name
 36      { get;set; }
 37      public string Address
 38      { get; set; }
 39      public string Tel
 40      { get; set; }
 41      public string OrderCode
 42      { get; set; }
 43      public IList<OrderItem> OrderItemList
 44      { get; set; }
 45  }
 46  
 47  <!--     使用者控件      -->
 48  <%@ Control Language="C#" AutoEventWireup="true" CodeFile="MyControl.ascx.cs" Inherits="MyControl" %>
 49  <script type="text/C#" runat="server">
 50      protected void Page_Load(object sender, EventArgs e)
 51      {
 52          GridView1.DataSource = GetList();
 53          GridView1.DataBind();
 54      }
 55  
 56      //模拟資料源
 57      protected IList<OrderItem> GetList()
 58      {
 59          IList<OrderItem> list = new List<OrderItem>();
 60          OrderItem orderItem = new OrderItem("1", "Asus N75S", 8800, 2);
 61          list.Add(orderItem);
 62          ..........
 63          return list;
 64      }
 65  
 66      //自定義委托  
 67      public delegate void MyDelegate(object sender,MyEventArgs myEventArgs);
 68      //自定義事件 
 69      public event MyDelegate MyEvent;
 70      
 71      //按下Button時激發自定義事件
 72      protected void btn_click(object sender, EventArgs e)
 73      {
 74          if (MyEvent != null)
 75          {
 76              MyEventArgs myEventArgs = new MyEventArgs(labelName.Text, labelAddress.Text, labelTel.Text
 77                  , labelOrderCode.Text, GetList());
 78              MyEvent(this,myEventArgs);
 79          }
 80      }
 81  </script>
 82  <div>
 83     Name : <asp:Label ID="labelName" runat="server">Leslie</asp:Label><br />
 84     Address : <asp:Label ID="labelAddress" runat="server">ZhongShan University 2A 501</asp:Label><br />
 85     Tel : <asp:Label ID="labelTel" runat="server">13660123456</asp:Label><br />
 86     Order Code : <asp:Label ID="labelOrderCode" runat="server">A12012031223B0030</asp:Label><br /><br />
 87     <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CellPadding="5">
 88        <Columns>
 89            <asp:BoundField DataField="OrderItemID" HeaderText="ID"/>
 90            <asp:BoundField DataField="Goods" HeaderText="Goods"/>
 91            <asp:BoundField DataField="Price" HeaderText="Price"/>
 92            <asp:BoundField DataField="Count" HeaderText="Count"/>
 93        </Columns>
 94     </asp:GridView>
 95     <br />
 96     <asp:Button ID="btn" runat="server" Text="Account" OnClick="btn_click"/>
 97  </div>
 98  
 99  <!--    頁面處理      -->
100  <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs" Inherits="Default2" %>
101  <%@ Register Src="~/MyControl.ascx" TagPrefix="ascx" TagName="myControl" %>
102  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
103  
104  <html xmlns="http://www.w3.org/1999/xhtml">
105  <head runat="server">
106      <title></title>
107      <script type="text/C#" runat="server">
108         //在頁面定義使用者控件MyEvent事件的處理方法
109         protected void myControl_Click(object sender,MyEventArgs e)
110         {
111             //計算訂單總體價格
112             double totalPrice=0;
113             IList<OrderItem> list=e.OrderItemList;
114             foreach(OrderItem item in list)
115                 totalPrice+=item.Price*item.Count;
116             //展示訂單号及總體費用
117             labelOrderCode.Text = e.OrderCode;
118             labelTotalPrice.Text = totalPrice.ToString();
119         }
120      </script>
121  </head>
122  <body>
123      <form id="form1" runat="server">
124      <div>
125         <ascx:myControl ID="myControl" runat="server" OnMyEvent="myControl_Click"></ascx:myControl>
126         <br />
127          OrderCode : <asp:Label ID="labelOrderCode" runat="server"></asp:Label><br />
128          TotalPrice :  <asp:Label ID="labelTotalPrice" runat="server"></asp:Label>
129      </div>
130      </form>
131  </body>
132  </html>      
C#綜合揭秘——深入分析委托與事件

若對自定義事件不太熟悉的朋友很多時候會使用 UserControl.FindControl 的方式擷取使用者控件中的屬性,但當你深入了解自定義事件的開發過程以後,就能有效簡化開發的過程。

5.1 Lambda 的意義

在Framework 2.0 以前,聲明委托的唯一方法是通過方法命名,從Framework 2.0 起,系統開始支援匿名方法。

通過匿名方法,可以直接把一段代碼綁定給事件,是以減少了執行個體化委托所需的編碼系統開銷。

而在 Framework 3.0 開始,Lambda 表達式開始逐漸取代了匿名方法,作為編寫内聯代碼的首選方式。總體來說,Lambda 表達式的作用是為了使用更簡單的方式來編寫匿名方法,徹底簡化委托的使用方式。

5.2 回顧匿名方法的使用

匿名方法的使用已經在4.4節簡單介紹過,在此回顧一下。

使用下面的方式,可以通過匿名方法為Button的Click事件綁定處理方法。

1         static void Main(string[] args)
2         {
3             Button btn = new Button();
4             btn.Click+=delegate(object obj,EventArgs e){
5                 MessageBox.Show("Hello World !");
6             };
7         }      

總是使用 delegate(){......} 的方式建立匿名方法,令人不禁感覺郁悶。于是從Framework 3.0 起, Lambda 表達式開始出現。

5.3 簡單介紹泛型委托

在介紹 Lambda 表達式前,先介紹一下常用的幾個泛型委托。

5.3.1 泛型委托 Predicate<T>

早在Framework 2.0 的時候,微軟就為 List<T> 類添加了 Find、FindAll 、ForEach 等方法用作資料的查找。

public T Find ( Predicate<T> match)

public List<T> FindAll(Predicate<T>  match)

在這些方法中存在一個Predicate <T> 表達式,它是一個傳回bool的泛型委托,能接受一個任意類型的對象作為參數。

public delegate bool Predicate<T>(T obj)

在下面例子中,Predicate 委托綁定了參數為Person類的方法Match作為查詢條件,然後使用 FindAll 方法查找到合适條件的 List<Person> 集合。

1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             List<Person> list = GetList();
 6             //綁定查詢條件
 7             Predicate<Person> predicate = new Predicate<Person>(Match);
 8             List<Person> result = list.FindAll(predicate);
 9             Console.WriteLine(“Person count is : ” + result.Count);
10             Console.ReadKey();
11         }
12         //模拟源資料
13         static List<Person> GetList()
14         {
15             var personList = new List<Person>();
16             var person1 = new Person(1,"Leslie",29);
17             personList.Add(person1);
18             ........
19             return personList;
20         }
21         //查詢條件
22         static bool Match(Person person)
23         {
24             return person.Age <= 30;
25         }
26     }
27 
28     public class Person
29     {
30         public Person(int id, string name, int age)
31         {
32             ID = id;
33             Name = name;
34             Age = age;
35         }
36 
37         public int ID
38         { get; set; }
39         public string Name
40         { get; set; }
41         public int Age
42         { get; set; }
43     }      

5.3.2 泛型委托 Action

Action<T> 的使用方式與 Predicate<T> 相似,不同之處在于 Predicate<T> 傳回值為 bool ,  Action<T> 的傳回值為 void。

Action 支援0~16個參數,可以按需求任意使用。

public delegate void Action()

public delegate void Action<T1>(T1 obj1)

public delegate void Action<T1,T2> (T1 obj1, T2 obj2)

public delegate void Action<T1,T2,T3> (T1 obj1, T2 obj2,T3 obj3)

............

public delegate void Action<T1,T2,T3,......,T16> (T1 obj1, T2 obj2,T3 obj3,......,T16 obj16)

1         static void Main(string[] args)
 2         {
 3             Action<string> action=ShowMessage;
 4             action("Hello World");
 5             Console.ReadKey();
 6         }
 7 
 8         static void ShowMessage(string message)
 9         {
10             MessageBox.Show(message);
11         }      

5.3.3 泛型委托 Func

委托 Func 與 Action 相似,同樣支援 0~16 個參數,不同之處在于Func 必須具有傳回值

public delegate TResult Func<TResult>()

public delegate TResult Func<T1,TResult>(T1 obj1)

public delegate TResult Func<T1,T2,TResult>(T1 obj1,T2 obj2)

public delegate TResult Func<T1,T2,T3,TResult>(T1 obj1,T2 obj2,T3 obj3)

public delegate TResult Func<T1,T2,T3,......,T16,TResult>(T1 obj1,T2 obj2,T3 obj3,......,T16 obj16)

1         static void Main(string[] args)
 2         {
 3             Func<double, bool, double> func = Account;
 4             double result=func(1000, true);
 5             Console.WriteLine("Result is : "+result);
 6             Console.ReadKey();
 7         }
 8 
 9         static double Account(double a,bool condition)
10         {
11             if (condition)
12                 return a * 1.5;
13             else
14                 return a * 2;
15         }      

5.4 揭開 Lambda 神秘的面紗

Lambda 的表達式的編寫格式如下:

     x=> x * 1.5

當中 “ => ” 是 Lambda 表達式的操作符,在左邊用作定義一個參數清單,右邊可以操作這些參數。

例子一, 先把 int x 設定 1000,通過 Action 把表達式定義為 x=x+500 ,最後通過 Invoke 激發委托。

1         static void Main(string[] args)
2         {
3             int x = 1000;
4             Action action = () => x = x + 500;
5             action.Invoke();
6 
7             Console.WriteLine("Result is : " + x);
8             Console.ReadKey();
9         }      

例子二,通過 Action<int> 把表達式定義 x=x+500, 到最後輸入參數1000,得到的結果與例子一相同。

注意,此處Lambda表達式定義的操作使用 { } 括弧包括在一起,裡面可以包含一系列的操作。

1         static void Main(string[] args)
 2         {
 3             Action<int> action = (x) =>
 4             {
 5                 x = x + 500;
 6                 Console.WriteLine("Result is : " + x);
 7             };
 8             action.Invoke(1000);
 9             Console.ReadKey();
10         }      

例子三,定義一個Predicate<int>,當輸入值大約等于1000則傳回 true , 否則傳回 false。與5.3.1的例子相比,Predicate<T>的綁定不需要顯式建立一個方法,而是直接在Lambda表達式裡完成,簡潔友善了不少。

1         static void Main(string[] args)
 2         {
 3             Predicate<int> predicate = (x) =>
 4             {
 5                 if (x >= 1000)
 6                     return true;
 7                 else
 8                     return false;
 9             };
10             bool result=predicate.Invoke(500);
11             Console.ReadKey();
12         }      

例子四,在計算商品的價格時,當商品重量超過30kg則打9折,其他按原價處理。此時可以使用Func<double,int,double>,參數1為商品原價,參數2為商品重量,最後傳回值為 double 類型。

1         static void Main(string[] args)
 2         {
 3             Func<double, int, double> func = (price, weight) =>
 4             {
 5                 if (weight >= 30)
 6                     return price * 0.9;
 7                 else
 8                     return price;
 9             };
10             double totalPrice = func(200.0, 40);
11             Console.ReadKey();
12         }      

例子五,使用Lambda為Button定義Click事件的處理方法。與5.2的例子相比,使用Lambda比使用匿名方法更加簡單。

1         static void Main(string[] args)
2         {
3             Button btn = new Button();
4             btn.Click += (obj, e) =>
5             {
6                 MessageBox.Show("Hello World!");
7             };
8             Console.ReadKey();
9         }      

例子六,此處使用5.3.1的例子,在List<Person>的FindAll方法中直接使用Lambda表達式。

相比之下,使用Lambda表達式,不需要定義Predicate<T>對象,也不需要顯式設定綁定方法,簡化了不工序。

1      class Program
 2      {
 3         static void Main(string[] args)
 4         {
 5             List<Person> personList = GetList();
 6             
 7             //查找年齡少于30年的人
 8             List<Person> result=personList.FindAll((person) => person.Age =< 30);
 9             Console.WriteLine("Person count is : " + result.Count);
10             Console.ReadKey();
11         }
12 
13          //模拟源資料
14          static List<Person> GetList()
15          {
16              var personList = new List<Person>();
17              var person1 = new Person(1,"Leslie",29);
18              personList.Add(person1);
19              .......
20              return personList;
21          }
22      }
23  
24      public class Person
25      {
26          public Person(int id, string name, int age)
27          {
28              ID = id;
29              Name = name;
30              Age = age;
31          }
32  
33          public int ID
34          { get; set; }
35          public string Name
36          { get; set; }
37          public int Age
38          { get; set; }
39      }      

當在使用LINQ技術的時候,到處都會彌漫着 Lambda 的身影,此時更能展現 Lambda 的長處。

但 LINQ 涉及到分部類,分部方法,IEnumerable<T>,疊代器等多方面的知識,這些已經超出本章的介紹範圍。

通過這一節的介紹,希望能夠幫助大家更深入地了解 Lambda 的使用。

 回到目錄

本章小結

本章主要介紹了委托(Delegate)的使用,委托對象是一個派生自 System.MultcastDelegate 的類,它能通過 Invoke 方式進行同步調用,也可以通過 BeginInvoke,EndInvoke 方式實作異步調用。而事件(Event)屬于一種特殊的委托,它與委托類型同步使用,可以簡化的開發過程。

最後,本文還介紹了匿名方法的使用方式,以及 Lambda 表達式的由來。

對 .NET 開發有興趣的朋友歡迎加入QQ群:230564952 共同探讨 !

C#綜合揭秘

通過修改系統資料庫建立Windows自定義協定

Entity Framework 并發處理詳解

細說程序、應用程式域與上下文

細說多線程(上)

細說多線程(下)

細說事務

深入分析委托與事件

作者:風塵浪子

http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html

原創作品,轉載時請注明作者及出處

原文連結:http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html

繼續閱讀