委托
委托是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托方法的使用可以像其他任何方法一样,具有参数和返回值,如下面的示例所示:
public delegate int PerformCalculation(int x, int y);
与委托的签名(由返回类型和参数组成)匹配的任何方法都可以分配给该委托。这样就可以通过编程方式来更改方法调用,还可以向现有类中插入新代码。只要知道委托的签名,便可以分配自己的委托方法。
委托具有以下特点:
l 委托类似于C++函数指针,但它是类型安全的
l 委托允许将方法作为参数进行传递
l 委托可用于定义回调方法
l 委托可以链接在一起,例如,可以对一个事件调用多个方法
l 方法不需要与委托签名精确匹配
l C# 2.0版引入了匿名方法的概念,此类方法允许将代码块作为参数传递,以代替单独定义的方法
使用委托
委托是一种安全地封装方法的类型,它与C和C++的函数指针类似。与C中的函数指针不同,委托是面向对象的、类型安全的和保险的。委托的类型由委托的名称定义。下面的示例声明了一个名为Del的委托,该委托可以封装一个采用字符串作为参数并返回void的方法。
public delegate void Del (string message);
构造委托对象时,通常提供委托将包装的方法的名称或使用匿名方法。实例化委托后,委托将把对它进行的方法调用传递给方法。调用方传递给委托的参数被传递给方法,来自方法的返回值(如果有)由委托返回给调用方。这被称为调用委托。可以将一个实例化的委托视为被包装的方法本身来调用该委托。例如:
// Create a method for a delegate.
public static void DelegateMethod(string message)
{
System.Console.WriteLine(message);
}
// Instantiate the delegate.
Del handler = DelegateMethod;
// Call the delegate.
handler("Hello World");
委托类型派生自.NET Framework中的Delegate类。委托类型是密封的,不能从Delegate中派生委托类型,也不可能从中派生自定义类。由于实例化委托是一个对象,所以可以将其作为参数进行传递,也可以将其赋值给属性。这样,方法便可以将一个委托作为参数来接受,并且以后可以调用该委托。这称为异步回调,是在较长的进程完成后用来通知调用方的常用方法。以这种方式使用委托时,使用委托的代码无需了解有关所用方法的实现方面的任何信息。此功能类似于接口所提供的封装。
回调的另一个常见用法是定义自定义的比较方法并将该委托传递给排序方法。它允许调用方的代码成为排序算法的一部分。下面的示例方法使用Del类型作为参数:
public void MethodWithCallback(int param1, int param2, Del callback)
{
callback("The number is: " + (param1 + param2).ToString());
}
然后可以将上面创建的委托传递给该方法:
MethodWithCallback(1, 2, handler);
在将委托用作抽象概念时,MethodWithCallback不需要直接调用控制台--设计它时无需考虑控制台。MethodWithCallback的作用只是准备字符串并将该字符串传递给其他方法。此功能特别强大,因为委托的方法可以使用任意数量的参数。
将委托构造为包装实例方法时,该委托将同时引用实例和方法。除了它所包装的方法外,委托不了解实例类型,所以只要任意类型的对象中具有与委托签名相匹配的方法,委托就可以引用该对象。将委托构造为包装静态方法时,它只引用方法。考虑下列声明:
public class MethodClass
{
public void Method1(string message) { }
public void Method2(string message) { }
}
加上前面显示的静态DelegateMethod,现在我们有三个方法可由Del实例进行包装。
调用委托时,它可以调用多个方法。这称为多路广播。若要向委托的方法列表(调用列表)中添加额外的方法,只需使用加法运算符或加法赋值运算符(“+”或“+=”)添加两个委托。例如:
MethodClass obj = new MethodClass();
Del d1 = obj.Method1;
Del d2 = obj.Method2;
Del d3 = DelegateMethod;
//Both types of assignment are valid.
Del allMethodsDelegate = d1 + d2;
allMethodsDelegate += d3;
此时,allMethodsDelegate在其调用列表中包含三个方法--Method1、Method2和DelegateMethod。原来的三个委托d1、d2和d3保持不变。调用allMethodsDelegate时,将按顺序调用所有这三个方法。如果委托使用引用参数,则引用将依次传递给三个方法的每个方法,由一个方法引起的更改对下一个方法是可见的。如果任一方法引发了异常,而在该方法内未捕获该异常,则该异常将传递给委托的调用方,并且不再对调用列表中后面的方法进行调用。如果委托具有返回值和/或输出参数,它将返回最后调用的方法的返回值和参数。若要从调用列表中移除方法,请使用减法运算符或减法赋值运算符(“-”或“-=”)。例如:
//remove Method1
allMethodsDelegate -= d1;
// copy AllMethodsDelegate while removing d2
Del oneMethodDelegate = allMethodsDelegate - d2;
由于委托类型派生自System.Delegate,所以可在委托上调用该类定义的方法和属性。例如,为了找出委托的调用列表中的方法数,您可以编写下面的代码:
int invocationCount = d1.GetInvocationList().GetLength(0);
在调用列表中具有多个方法的委托派生自MulticastDelegate,这是System.Delegate的子类。由于两个类都支持GetInvocationList,所以上面的代码在两种情况下都适用。
多路广播委托广泛用于事件处理中。事件源对象向已注册接受该事件的接收方对象发送事件通知。为了为事件注册,接收方创建了旨在处理事件的方法,然后为该方法创建委托并将该委托传递给事件源。事件发生时,源将调用委托。然后,委托调用接收方的事件处理方法并传递事件数据。给定事件的委托类型由事件源定义。
在编译时,对分配了两种不同类型的委托进行比较将产生编译错误。如果委托实例静态地属于类型System.Delegate,则允许进行比较,但在运行时将返回false。例如:
delegate void Delegate1();
delegate void Delegate2();
static void method(Delegate1 d, Delegate2 e, System.Delegate f)
{
// Compile-time error.
//Console.WriteLine(d == e);
// OK at compile-time. False if the run-time type of f
//is not the same as that of d.
System.Console.WriteLine(d == f);
}
命名方法
委托可以与命名方法关联。使用命名的方法对委托进行实例化时,该方法将作为参数传递,例如:
// Declare a delegate:
delegate void Del (int x);
// Define a named method:
void DoWork(int k) { /* ... */ }
// Instantiate the delegate using the method as a parameter:
Del d = obj.DoWork;
这被称为使用命名的方法。使用命名方法构造的委托可以封装静态方法或实例方法。在以前的C#版本中,使用命名的方法是对委托进行实例化的唯一方式。但是,在创建新方法的系统开销不必要时,C# 2.0允许对委托进行实例化,并立即指定委托将在被调用时处理的代码块。这些被称为匿名方法。
备注
作为委托参数传递的方法必须与委托声明具有相同的签名。
委托实例可以封装静态或实例方法。
尽管委托可以使用out参数,但不推荐将其用于多路广播事件委托中,因为无法确定要调用哪个委托。
示例1
以下是声明及使用委托的一个简单示例。注意,委托Del和关联的方法MultiplyNumbers具有相同的签名
delegate void Del( int i, double j);
class MathClass {
static void Main(){
MathClass m = new MathClass();
//Delegate instantiation using "MultiplyNumbers"
Del d = m.MultiplyNumbers;
//Invoke the delegate object.
System.Console.WriteLine("Invoking the delegate using 'MultiplyNumbers':");
for (int i = 1; i <= 5; i++){
d(i,2);
}
}
//Declare the associated method.
void MultiplyNumbers(int m,double n){
System.Console.Write(m * n + " ");
}
}
示例2
在下面的示例中,一个委托被同时映射到静态方法和实例方法,并分别返回特定的信息。
delegate void Del();
class SampleClass {
public void InstanceMethod(){
System.Console.WriteLine("A message from the instance method.");
}
static public void StaticMethod(){
System.Console.WriteLine("A message from the static method.");
}
}
class TestSampleClass {
static void Main(){
SampleClass sc = new SampleClass();
//Map the delegate to the instance method:
Del d = sc.InstanceMethod;
d();
//Map to the static method:
d = SampleClass.StaticMethod;
d();
}
}
匿名方法
在2.0之前的C#版本中,声明委托的唯一方法是使用命名方法。C# 2.0引入了匿名方法。
要将代码块传递给委托参数,创建匿名方法则是唯一的方法。例如:
// Create a handler for a click event
button1.Click += delegate(System.Object o, System.EventArgs e)
{ System.Windows.Forms.MessageBox.Show("Click!"); };
或
// Create a delegate instance
delegate void Del (int x);
// Instantiate the delegate using an anonymous method
Del d = delegate(int k) { /* ... */ };
如果使用匿名方法,则不必创建单独的方法,因此减少了实例化委托所需的编码系统开销。
例如,如果创建方法所需的系统开销是不必要的,在委托的位置指定代码块就非常有用。启动新线程即是一个很好的示例。无需为委托创建更多方法,线程类即可创建一个线程并包含该线程执行的代码。
void StartThread()
{
System.Threading.Thread t1 = new System.Threading.Thread
(delegate()
{
System.Console.Write("Hello, ");
System.Console.WriteLine("World!");
});
t1.Start();
}
备注
匿名方法的参数的范围是anonymous-method-block。
在目标的快外部的匿名方法块内使用跳转语句(如goto、break或continue)是错误的。在目标在块内部的匿名方法块外部使用跳转语句(如goto、break或continue)也是错误的。
如果局部变量和参数的范围包含匿名方法声明,则该局部变量和参数称为该匿名方法的外部变量或捕获变量。例如,下面代码中的n即是一个外部变量:
int n = 0;
Del d = delegate() { System.Console.WriteLine("Copy #:{0}", ++n); };
与局部变量不同,外部变量的声明周期一直持续到引用该匿名方法的委托符合垃圾回收的条件为止。对n的引用是在创建该委托时捕获的。匿名方法不能访问外部范围的ref或out参数。
在anonymous-method-block中不能访问任何不安全代码。
示例
下面的示例演示实例化委托的两种方法:
l 使委托与匿名方法关联。
l 是委托与命名方法(DoWork)关联。
两种方法都会在调用委托时显示一条消息。
delegate void Printer( string s);
class TestClass {
static void Main(){
//Instatiate the delegate type using an anonymous method:
Printer p = delegate(string j){
System.Console.WriteLine(j);
};
//Results from the anonymous delegate call:
p("The delegate using the anonymous method is called.");
//The delegate instantiation using a named method "DoWork":
p = new Printer(TestClass.DoWork);
//Results from the old style delegate call:
p("The delegate using the named method is called.");
}
//The method associated with the named delegate:
static void DoWork(string k){
System.Console.WriteLine(k);
}
}