委托与事件
Ganesh Nataraj最近写了一篇解释委托与事件的文章,在坊间流传较广,今天翻译成中文与大家共享,如有不妥之处,欢迎留言讨论。
C#中的委托类似于C或C++中的函数指针。程序设计人员可以使用委托将方法的引用压缩到委托对象中,委托对象能被传递给调用该方法引用的代码而无须知道哪个方法将在编译时被调用。与C或C++中的指针不同的是,委托是面向对象的、类型安全的、受保护的。
委托声明时定义一个返回压缩方法的类型,其中包含一组特定的描述和返回类型。对于静态方法而言,委托对象压缩其调用的方法。对于实例方法(instance methods)而言,委托对象将压缩一个实例和实例的一个方法。如果一个委托对象有一组适当的描述,可以调用带描述的委托。
委托有趣而实用的一个特征就是它不用知道也无需关心它引用对象的类,任何对象都可以,关键的是方法的描述类型和引用类型要与委托的匹配。这使委托特别适合一些匿名的请求。<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
注意:委托以调用方的安全许可身份运行,而不是以声明方的许可运行。
下面有两个委托的示例:
例1向大家说明如何声明、实例化和调用一个委托;
例2向大家说明如何联合两个委托。
例1
这个例子说明如何声明、实例化和使用委托。BookDB类压缩了一个包含各种书籍的书店数据库,它对外暴露ProcessPaperbackBooks方法,用以查找数据库中所有平装本的书籍并调用委托,使用委托来调用ProcessBookDelegate。Test类使用这个类来打印处平装本书籍的标题和平均价格。
委托的使用促进了书店数据库与客户端代码之间功能性的良好分离。客户端代码不用知晓书是如何存的如何找到平装本,而书店的代码不用知道查找到该平装书并提供给客户端后将会被如何处理。代码如下(为了部影响理解,代码保持原样):
1
// bookstore.cs
2
using System;
3
// A set of classes for handling a bookstore:
4
namespace 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:
52
namespace 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
2
using System;
3
delegate void MyDelegate(string s);
4
class 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. 接口
委托与接口在都能促成规范与执行的分离,有相似之处。那声明时候使用委托声明时候使用接口呢?大体依据以下原则:
如下情况宜使用委托:
- 只调用单个方法时.
- 当一个类需要方法说明的多重执行时.
- 期望使用静态方法执行规范时.
- 期望得到一个类似事件的模式时.
- 调用者无需知道无需获取定义方法的对象时
- 只想给少数既定组件分发执行规范时.
- 想要简单的组成结构时.
如下情况宜使用接口:
- 当规范定义了一组需要调用的相关方法时.
- 一个类仅代表性地执行一次规范时.
- 接口的调用者想映射接口类型以获取其他类或接口时