Ganesh Nataraj最近寫了一篇解釋委托與事件的文章,在坊間流傳較廣,今天翻譯成中文與大家共享,如有不妥之處,歡迎留言討論。
C#中的委托類似于C或C++中的函數指針。程序設計人員可以使用委托將方法的引用壓縮到委托對象中,委托對象能被傳遞給調用該方法引用的代碼而無須知道哪個方法將在編譯時被調用。與C或C++中的指針不同的是,委托是面向對象的、類型安全的、受保護的。
委托聲明時定義一個返回壓縮方法的類型,其中包含一組特定的描述和返回類型。對于靜態方法而言,委托對象壓縮其調用的方法。對于實例方法(instance methods)而言,委托對象將壓縮一個實例和實例的一個方法。如果一個委托對象有一組適當的描述,可以調用帶描述的委托。
委托有趣而實用的一個特征就是它不用知道也無需關心它引用對象的類,任何對象都可以,關鍵的是方法的描述類型和引用類型要與委托的匹配。這使委托特別適合一些匿名的請求。
注意:委托以調用方的安全許可身份運行,而不是以聲明方的許可運行。
下面有兩個委托的示例:
例1向大家說明如何聲明、實例化和調用一個委托;
例2向大家說明如何聯合兩個委托。
例1
這個例子說明如何聲明、實例化和使用委托。BookDB類壓縮了一個包含各種書籍的書店數據庫,它對外暴露ProcessPaperbackBooks方法,用以查找數據庫中所有平裝本的書籍并調用委托,使用委托來調用ProcessBookDelegate。Test類使用這個類來打印處平裝本書籍的標題和平均價格。
委托的使用促進了書店數據庫與客戶端代碼之間功能性的良好分離??蛻舳舜a不用知曉書是如何存的如何找到平裝本,而書店的代碼不用知道查找到該平裝書并提供給客戶端后將會被如何處理。代碼如下(為了部影響理解,代碼保持原樣):
1// bookstore.cs
2using System;
3// A set of classes for handling a bookstore:
4namespace Bookstore
5{
6 using System.Collections;
7 // Describes a book in the book list:
8 public struct Book
9 {
10 public string Title; // Title of the book.
11 public string Author; // Author of the book.
12 public decimal Price; // Price of the book.
13 public bool Paperback; // Is it paperback?
14 public Book(string title, string author, decimal price, bool paperBack)
15 {
16 Title = title;
17 Author = author;
18 Price = price;
19 Paperback = paperBack;
20 }
21 }
22
23 // Declare a delegate type for processing a book:
24 public delegate void ProcessBookDelegate(Book book);
25
26 // Maintains a book database.
27 public class BookDB
28 {
29 // List of all books in the database:
30 ArrayList list = new ArrayList();
31
32 // Add a book to the database:
33 public void AddBook(string title, string author, decimal price, bool paperBack)
34 {
35 list.Add(new Book(title, author, price, paperBack));
36 }
37
38 // Call a passed-in delegate on each paperback book to process it:
39 public void ProcessPaperbackBooks(ProcessBookDelegate processBook)
40 {
41 foreach (Book b in list)
42 {
43 if (b.Paperback)
44
45 // Calling the delegate:
46 processBook(b);
47 }
48 }
49 }
50}
51// Using the Bookstore classes:
52namespace BookTestClient
53{
54 using Bookstore;
55
56 // Class to total and average prices of books:
57 class PriceTotaller
58 {
59 int countBooks = 0;
60 decimal priceBooks = 0.0m;
61 internal void AddBookToTotal(Book book)
62 {
63 countBooks += 1;
64 priceBooks += book.Price;
65 }
66 internal decimal AveragePrice()
67 {
68 return priceBooks / countBooks;
69 }
70 }
71 // Class to test the book database:
72 class Test
73 {
74 // Print the title of the book.
75 static void PrintTitle(Book b)
76 {
77 Console.WriteLine(" {0}", b.Title);
78 }
79 // Execution starts here.
80 static void Main()
81 {
82 BookDB bookDB = new BookDB();
83 // Initialize the database with some books:
84 AddBooks(bookDB);
85 // Print all the titles of paperbacks:
86 Console.WriteLine("Paperback Book Titles:");
87 // Create a new delegate object associated with the static
88 // method Test.PrintTitle:
89 bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));
90 // Get the average price of a paperback by using
91 // a PriceTotaller object:
92 PriceTotaller totaller = new PriceTotaller();
93 // Create a new delegate object associated with the nonstatic
94 // method AddBookToTotal on the object totaller:
95 bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(totaller.AddBookToTotal));
96 Console.WriteLine("Average Paperback Book Price: ${0:#.##}",
97 totaller.AveragePrice());
98 }
99 // Initialize the book database with some test books:
100 static void AddBooks(BookDB bookDB)
101 {
102 bookDB.AddBook("The C Programming Language",
103 "Brian W. Kernighan and Dennis M. Ritchie", 19.95m, true);
104 bookDB.AddBook("The Unicode Standard 2.0",
105 "The Unicode Consortium", 39.95m, true);
106 bookDB.AddBook("The MS-DOS Encyclopedia",
107 "Ray Duncan", 129.95m, false);
108 bookDB.AddBook("Dogbert's Clues for the Clueless",
109 "Scott Adams", 12.00m, true);
110 }
111 }
112}
113
輸出:
平裝書的標題:
The C Programming Language
The Unicode Standard 2.0
Dogbert's Clues for the Clueless
平均價格: $23.97
討論:
委托的聲明
委托可聲明如下:
public delegate void ProcessBookDelegate(Book book);
聲明一個新的委托類型。每個委托類型可包含委托描述的數量和類型,包含被壓縮方法返回值的類型。不管是否需要一組類型描述或返回值類型,必須聲明一個新的委托類型。
實例化一個委托: 擋一個委托的類型被聲明后,必須創建委托對象并與一個特定的方法相關聯。和其他對象一樣,需要一起創建新的委托對象和新的表達式。
看看這段:
bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));
聲明一個關聯靜態方法Test.PrintTitle的委托對象。
再看看這段:
bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(totaller.AddBookToTotal));
創建一個委托對象關聯totaller對象上的非靜態方法 AddBookToTotal。新的委托對象馬上被傳遞給ProcessPaperbackBooks方法。
請注意,委托一旦被創建后,其關聯的方法將不能再被更改,因為委托對象是不可變的。
調用委托:委托對象被創建后會被傳遞給調用委托的其他代碼。通過委托對象名稱和其后跟隨的括號化描述來調用委托對象,示例如下。
processBook(b);
如例中所示,委托可以被同步調用,也可以使用BeginInvoke和EndInvoke異步調用。
例2(示范如何聯合兩個委托)
這個例子示范了委托的構成,委托對象的一個有用屬性是他們可以使用”+”運算符來進行聯合,聯合委托調用組成它的兩個委托,只有類型相同的委托才可以聯合。”-”操作符用于從聯合委托中移除一個委托。示例代碼如下:
1// compose.cs
2using System;
3delegate void MyDelegate(string s);
4class MyClass
5{
6 public static void Hello(string s)
7 {
8 Console.WriteLine(" Hello, {0}!", s);
9 }
10 public static void Goodbye(string s)
11 {
12 Console.WriteLine(" Goodbye, {0}!", s);
13 }
14 public static void Main()
15 {
16 MyDelegate a, b, c, d;
17 // Create the delegate object a that references
18 // the method Hello:
19 a = new MyDelegate(Hello);
20 // Create the delegate object b that references
21 // the method Goodbye:
22 b = new MyDelegate(Goodbye);
23 // The two delegates, a and b, are composed to form c:
24 c = a + b;
25 // Remove a from the composed delegate, leaving d,
26 // which calls only the method Goodbye:
27 d = c - a;
28 Console.WriteLine("Invoking delegate a:");
29 a("A");
30 Console.WriteLine("Invoking delegate b:");
31 b("B");
32 Console.WriteLine("Invoking delegate c:");
33 c("C");
34 Console.WriteLine("Invoking delegate d:");
35 d("D");
36 }
37}
38
輸出:
調用委托 a:
Hello, A!
調用委托b:
Goodbye, B!
調用委托c:
Hello, C!
Goodbye, C!
調用委托d:
Goodbye, D!
委托與事件
對于給組件的“聽眾”來通知該組件的發生的事件而言,使用委托特別適合。
委托 VS. 接口
委托與接口在都能促成規范與執行的分離,有相似之處。那聲明時候使用委托聲明時候使用接口呢?大體依據以下原則:
如下情況宜使用委托:
只調用單個方法時.
當一個類需要方法說明的多重執行時.
期望使用靜態方法執行規范時.
期望得到一個類似事件的模式時.
調用者無需知道無需獲取定義方法的對象時
只想給少數既定組件分發執行規范時.
想要簡單的組成結構時.
如下情況宜使用接口:
當規范定義了一組需要調用的相關方法時.
一個類僅代表性地執行一次規范時.
接口的調用者想映射接口類型以獲取其他類或接口時