目录
C# 委托(Delegate)
委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联。 你可以通过委托实例调用方法。
委托用于将方法作为参数传递给其他方法。 事件处理程序就是通过委托调用的方法。 你可以创建一个自定义方法,当发生特定事件时,某个类(如 Windows 控件)就可以调用你的方法。 下面的示例演示了一个委托声明:
public delegate int PerformCalculation(int x, int y);
可将任何可访问类或结构中与委托类型匹配的任何方法分配给委托。 该方法可以是静态方法,也可以是实例方法。 此灵活性意味着你可以通过编程方式来更改方法调用,还可以向现有类中插入新代码。
注意:在方法重载的上下文中,方法的签名不包括返回值。 但在委托的上下文中,签名包括返回值。 换句话说,方法和委托必须具有相同的返回类型。
将方法作为参数进行引用的能力使委托成为定义回调方法的理想选择。 可编写一个比较应用程序中两个对象的方法。 该方法可用在排序算法的委托中。 由于比较代码与库分离,因此排序方法可能更常见。
对于类似的方案,已将函数指针添加到 C# 9,其中你需要对调用约定有更多的控制。 使用添加到委托类型的虚方法调用与委托关联的代码。 使用函数指针,可以指定不同的约定。
委托概述
委托具有以下属性:
- 类似于C++的函数指针,但是完全面向对象。与C++的函数指针不同,委托会同时封装对象实例和方法。
- 允许将方法作为参数进行传递,这使得委托非常灵活,可以用于定义回调方法。
- 可以将多个委托链接在一起,例如,可以对一个事件调用多个方法,从而实现多播委托的功能。
- 方法不必与委托类型完全匹配,这意味着可以使用变体来适应不同的方法签名。有关详细信息,请参阅使用委托中的变体。
- 使用Lambda表达式可以更简洁地编写内联代码块。在某些上下文中,Lambda表达式可以编译为委托类型,从而进一步简化了代码编写。 若要详细了解 lambda 表达式,请参阅 lambda 表达式。
总的来说,委托作为C#中的重要特性,为开发人员提供了一种非常灵活和强大的方法,用于处理回调、事件处理以及动态方法调用等场景。
使用委托
委托是安全封装方法的类型,类似于 C 和 C++ 中的函数指针。 与 C 函数指针不同的是,委托是面向对象的、类型安全的和可靠的。 委托的类型由委托的名称确定。 以下示例声明名为 Callback
的委托,该委托可以封装采用字符串作为参数并返回 void 的方法:
public delegate void Callback(string message);
委托对象通常可采用两种方式进行构造,一种是提供委托将封装的方法的名称,另一种是使用 lambda 表达式。 以这种方式实例化委托后,可以调用该委托。 调用委托会调用附加到委托实例的方法。 调用方传递到委托的参数将传递到该方法,并且委托会将方法的返回值(如果有)返回到调用方。 例如:
// 为委托创建方法
public static void DelegateMethod(string message)
{
Console.WriteLine(message);
}
// 实例化委托。
Callback handler = DelegateMethod;
// 使用委托。
handler("Hello World");
委托类型派生自 .NET 中的 Delegate 类。 委托类型是密封的,它们不能派生自 Delegate,也不能从其派生出自定义类。 由于实例化的委托是一个对象,因此可以作为实参传递或分配给一个属性。 这允许方法接受委托作为参数并在稍后调用委托。 这被称为异步回调,是在长进程完成时通知调用方的常用方法。 当以这种方式使用委托时,使用委托的代码不需要知道要使用的实现方法。 功能类似于封装接口提供的功能。
回调的另一个常见用途是定义自定义比较方法并将该委托传递到短方法。 它允许调用方的代码成为排序算法的一部分。 以下示例方法使用 Del 类型作为参数:
public static void MethodWithCallback(int param1, int param2, Callback callback)
{
callback("The number is: " + (param1 + param2).ToString());
}
然后,你可以将上面创建的委托传递到该方法:
MethodWithCallback(1, 2, handler);
并将以下输出接收到控制台:
The number is: 3
以抽象方式使用委托时,MethodWithCallback 不需要直接调用控制台,记住,其不必设计为具有控制台。 MethodWithCallback 的作用是简单准备字符串并将字符串传递到其他方法。 由于委托的方法可以使用任意数量的参数,此功能特别强大。
当委托构造为封装实例方法时,委托将同时引用实例和方法。 委托不知道除其所封装方法以外的实例类型,因此委托可以引用任何类型的对象,只要该对象上有与委托签名匹配的方法。 当委托构造为封装静态方法时,委托仅引用方法。 请考虑以下声明:
public class MethodClass
{
public void Method1(string message) { }
public void Method2(string message) { }
}
加上之前显示的静态 DelegateMethod,我们现在已有三个 Del 实例可以封装的方法。
调用时,委托可以调用多个方法。 这被称为多播。 若要向委托的方法列表(调用列表)添加其他方法,只需使用加法运算符或加法赋值运算符(“+”或“+=”)添加两个委托。 例如:
var obj = new MethodClass();
Callback d1 = obj.Method1;
Callback d2 = obj.Method2;
Callback d3 = DelegateMethod;
//这两种类型的分配都是有效的
Callback allMethodsDelegate = d1 + d2;
allMethodsDelegate += d3;
此时,allMethodsDelegate 的调用列表中包含三个方法,分别为 Method1、Method2 和 DelegateMethod。 原有的三个委托(d1、d2 和 d3)保持不变。 调用 allMethodsDelegate 时,将按顺序调用所有三个方法。 如果委托使用引用参数,引用将按相反的顺序传递到所有这三个方法,并且一种方法进行的任何更改都将在另一种方法上见到。 当方法引发未在方法内捕获到的异常时,该异常将传递到委托的调用方,并且不会调用调用列表中的后续方法。 如果委托具有返回值和/或输出参数,它将返回上次调用方法的返回值和参数。 若要删除调用列表中的方法,请使用减法运算符或减法赋值运算符(- 或 -=)。 例如:
//remove Method1
allMethodsDelegate -= d1;
// 删除 d2 时复制 AllMethodsDelegate
Callback oneMethodDelegate = allMethodsDelegate - d2;
由于委托类型派生自 System.Delegate,因此可以在委托上调用该类定义的方法和属性。 例如,若要查询委托调用列表中方法的数量,你可以编写:
int invocationCount = d1.GetInvocationList().GetLength(0);
调用列表中具有多个方法的委托派生自 MulticastDelegate,该类属于 System.Delegate 的子类。 由于这两个类都支持 GetInvocationList,因此在其他情况下,上述代码也将产生作用。
多播委托广泛用于事件处理中。 事件源对象将事件通知发送到已注册接收该事件的接收方对象。 若要注册一个事件,接收方需要创建用于处理该事件的方法,然后为该方法创建委托并将委托传递到事件源。 事件发生时,源调用委托。 然后,委托将对接收方调用事件处理方法,从而提供事件数据。 给定事件的委托类型由事件源确定。 有关详细信息,请参阅事件。
在编译时比较分配的两个不同类型的委托将导致编译错误。 如果委托实例是静态的 System.Delegate 类型,则允许比较,但在运行时将返回 false。 例如:
delegate void Callback1();
delegate void Callback2();
static void method(Callback1 d, Callback2 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.
Console.WriteLine(d == f);
}
带有命名方法的委托与匿名方法
委托可以与命名方法相关联。 使用命名方法实例化委托时,该方法作为参数传递,例如:
// 声明一个委托
delegate void WorkCallback(int x);
// 定义命名方法
void DoWork(int k) { /* ... */ }
// 使用方法作为参数实例化委托
WorkCallback d = obj.DoWork;
这称为使用命名方法。 使用命名方法构造的委托可以封装静态方法或实例方法。 命名方法是在早期版本的 C# 中实例化委托的唯一方式。 但是,如果创建新方法会造成多余开销,C# 允许你实例化委托并立即指定调用委托时委托将处理的代码块。 代码块可包含 Lambda 表达式或匿名方法。
作为委托参数传递的方法必须具有与委托声明相同的签名。 委托实例可以封装静态方法或实例方法。
注意:尽管委托可以使用 out 参数,但不建议将该委托与多播事件委托配合使用,因为你无法知道将调用哪个委托。
从 C# 10 开始,包含单个重载的方法组具有自然类型。 这意味着编译器可以推断委托类型的返回类型和参数类型:
var read = Console.Read; // 只有一个过载;Func<int>推断
var write = Console.Write; // 错误:多个重载,无法选择
示例
using System;
public delegate void MyDelegate(string message);
public class Program
{
public static void Main()
{
// 使用命名方法创建委托实例
MyDelegate delegate1 = new MyDelegate(Method1);
delegate1("来自命名方法的Hello");
// 使用匿名方法创建委托实例
MyDelegate delegate2 = delegate (string message)
{
Console.WriteLine("匿名方法: " + message);
};
delegate2("来自匿名方法的Hello");
}
// 命名方法
public static void Method1(string message)
{
Console.WriteLine("方法1: " + message);
}
}
在这个示例中,我们首先定义了一个名为MyDelegate的委托类型,它接受一个字符串参数并无返回值。然后,在Main方法中,我们创建了两个委托实例delegate1和delegate2。
其中,delegate1使用命名方法Method1来初始化,并调用delegate1来触发委托,从而调用关联的命名方法。
delegate2使用匿名方法来初始化,并调用delegate2来触发委托,从而调用关联的匿名方法。
如何合并委托(多播委托)
使用+运算符可以将多个委托对象分配到一个委托实例中,从而创建一个多播委托。这个多播委托包含了一个已经分配好的委托列表,调用多播委托时会依次调用列表中的委托。
只有类型相同的委托实例才能通过使用+运算符进行合并,否则会产生编译错误。
另外,- 运算符也可用于从多播委托中删除组件委托。例如,如果我们有一个多播委托delegateChain,其中包含了两个委托对象delegate1和delegate2,我们可以使用 - 运算符将其中一个委托对象从多播委托中删除:
delegateChain = delegateChain - delegate1; // 从多播委托中删除 delegate1
这将从delegateChain中删除delegate1,使得delegateChain中仅包含delegate2委托对象。
示例
using System;
public delegate void MyDelegate(string message);
class Program
{
static void Main()
{
MyDelegate delegate1 = new MyDelegate(Method1);
MyDelegate delegate2 = new MyDelegate(Method2);
MyDelegate delegate3 = new MyDelegate(Method3);
// 合并多个委托对象到一个多播委托
MyDelegate delegateChain = delegate1 + delegate2 + delegate3;
// 调用多播委托,将依次调用列表中的委托
delegateChain("第一次依次调用列表中的委托");
Console.WriteLine();
// 从多播委托中删除一个委托对象
delegateChain = delegateChain - delegate2;
// 再次调用多播委托,将不再调用已删除的委托对象
delegateChain("第二次依次调用列表中的委托");
}
static void Method1(string message)
{
Console.WriteLine("Method1: " + message);
}
static void Method2(string message)
{
Console.WriteLine("Method2: " + message);
}
static void Method3(string message)
{
Console.WriteLine("Method3: " + message);
}
}
运行结果如下所示:
Method1: 第一次依次调用列表中的委托
Method2: 第一次依次调用列表中的委托
Method3: 第一次依次调用列表中的委托
Method1: 第二次依次调用列表中的委托
Method3: 第二次依次调用列表中的委托
在这个示例中,我们定义了三个不同的委托对象delegate1、delegate2和delegate3,它们都指向不同的方法。我们使用+运算符将它们合并为一个多播委托delegateChain,并调用它来依次调用列表中的委托。
然后,我们使用-运算符从delegateChain中删除了一个委托对象delegate2。再次调用delegateChain时,已删除的委托对象delegate2不再被调用。
如何声明、实例化和使用委托
可以使用以下任一方法声明委托:
- 使用匹配签名声明委托类型并声明方法。
- 将方法组分配给委托类型。
- 声明匿名方法。
- 使用 lambda 表达式。有关详细信息,请参阅 Lambda 表达式。
以下是使用不同方法声明委托的示例代码:
using System;
// 1. 使用匹配签名声明委托类型并声明方法
public delegate void MyDelegate(string message);
class Program
{
static void Main()
{
// 2. 将方法分配给委托类型
MyDelegate delegate1 = new MyDelegate(Method1);
delegate1("将方法分配给委托类型");
// 3. 声明匿名方法并赋给委托
MyDelegate delegate2 = delegate (string message) { Console.WriteLine("匿名方法: " + message); };
delegate2("声明匿名方法并赋给委托");
// 4. 使用 lambda 表达式
MyDelegate delegate3 = (message) => { Console.WriteLine("Lambda表达式: " + message); };
delegate3("使用 lambda 表达式");
}
static void Method1(string message)
{
Console.WriteLine("Method1: " + message);
}
}
在这个示例中,我们演示了四种不同的方法来声明委托和分配方法:
- 我们首先使用匹配签名声明了一个MyDelegate类型的委托,并声明了一个方法Method1,它的签名和委托类型匹配。
- 然后,我们将命名方法Method1分配给了delegate1委托对象,并调用它。
- 接着,我们声明了一个匿名方法,并将其分配给了delegate2委托对象,并调用它。
- 最后,我们使用lambda表达式声明了一个方法,并将其分配给了delegate3委托对象,并调用它。
示例
以下是一个简单的示例,演示如何在C#中声明、实例化和使用委托:
using System;
// 声明一个委托类型,它接受两个整数参数并返回一个整数
public delegate int MyDelegate(int x, int y);
class Program
{
static void Main()
{
// 实例化委托对象并将其分配给命名方法
MyDelegate delegate1 = new MyDelegate(Method1);
// 使用委托对象调用已分配的方法
int result1 = delegate1(10, 5);
Console.WriteLine("结果1: " + result1);
// 重新分配委托对象到匿名方法
MyDelegate delegate2 = delegate (int x, int y) { return x - y; };
// 使用委托对象调用已分配的匿名方法
int result2 = delegate2(10, 5);
Console.WriteLine("结果2: " + result2);
// 重新分配委托对象到 lambda 表达式
MyDelegate delegate3 = (x, y) => x * y;
// 使用委托对象调用已分配的 lambda 表达式
int result3 = delegate3(10, 5);
Console.WriteLine("结果3: " + result3);
}
// 命名方法,它与 MyDelegate 委托类型具有相同的签名
static int Method1(int x, int y)
{
return x + y;
}
}
在这个示例中,我们首先声明了一个委托类型MyDelegate,它接受两个整数参数并返回一个整数。然后,我们实例化了三个委托对象,并将它们分配给不同的方法、匿名方法和lambda表达式。我们使用这些委托对象调用已分配的方法、匿名方法和lambda表达式,并输出结果。
可靠编程
1、声明委托类型:
public delegate void ProcessBookCallback(Book book);
每个委托类型描述自变量的数量和类型,以及它可以封装的方法的返回值类型。 每当需要一组新的自变量类型或返回值类型,则必须声明一个新的委托类型。
2、实例化委托:
声明委托类型后,则必须创建委托对象并将其与特定的方法相关联。 在上例中,你通过将 PrintTitle 方法传递给 ProcessPaperbackBooks 方法执行此操作,如下面的示例所示:
bookDB.ProcessPaperbackBooks(PrintTitle);
这将创建一个新的与静态方法 Test.PrintTitle 关联的委托对象。 同样,如下面的示例所示,传递对象 totaller 中的非静态方法 AddBookToTotal:
bookDB.ProcessPaperbackBooks(totaller.AddBookToTotal);
在这两种情况下,都将新的委托对象传递给 ProcessPaperbackBooks 方法。创建委托后,它与之关联的方法就永远不会更改;委托对象是不可变的。
3、调用委托
创建委托对象后,通常会将委托对象传递给将调用该委托的其他代码。 委托对象是通过使用委托对象的名称调用的,后跟用圆括号括起来的将传递给委托的自变量。 下面是一个委托调用示例:
processBook(b);
需要注意的是,委托可以同步调用,也可以异步调用,具体取决于应用程序的需要和设计。异步调用可以通过使用BeginInvoke和EndInvoke方法来实现。