System.Delegate 和 delegate 关键字

以前

本文介绍支持委托的 .NET 中的类,以及这些类如何映射到 delegate 关键字。

什么是委托?

将委托视为存储对方法的引用的方法,类似于存储对对象的引用的方式。 正如可以将对象传递给方法一样,可以使用委托传递方法引用。 如果要编写灵活的代码,其中可以“插入”不同的方法以提供不同的行为,这非常有用。

例如,假设你有一个计算器可以对两个数字执行运算。 你可以使用委托来表示任何接受两个数字并返回结果的运算,而不是将加法、减法、乘法和除法硬编码到单独的方法中。

定义委托类型

现在,让我们了解如何使用 delegate 关键字创建委托类型。 定义委托类型时,实质上是创建一个模板,用于描述该委托中可以存储哪种方法。

使用类似于方法签名的语法定义委托类型,但开头带有 delegate 关键字:

// Define a simple delegate that can point to methods taking two integers and returning an integer
public delegate int Calculator(int x, int y);

Calculator 委托可以保留对任何采用两个 int 参数并返回 int 的方法的引用。

让我们看看一个更实用的示例。 如果要对列表进行排序,需要告知排序算法如何比较项。 我们来了解一下委托如何帮助实现 List.Sort() 方法。 第一步是为比较操作创建委托类型。

// From the .NET Core library
public delegate int Comparison<in T>(T left, T right);

Comparison<T> 委托可以保留对以下任何方法的引用:

  • 接收两个T类型的参数
  • 返回一个 int (通常为 -1、0 或 1 以指示“小于”、“等于”或“大于”)

定义像这样的委托类型时,编译器会自动生成一个类,该类派生自 System.Delegate,并与您的签名匹配。 此类为你处理存储和调用方法引用的所有复杂性。

Comparison委托类型是泛型类型,这意味着它可以处理任何类型T。 有关泛型的详细信息,请参阅 泛型类和方法

请注意,即使语法看起来类似于声明变量,你实际上也声明了一个新 类型。 可以在类内、直接在命名空间内,甚至全局命名空间中定义委托类型。

注释

不建议直接在全局命名空间中声明委托类型(或其他类型)。

编译器还会为此新类型生成添加和删除处理程序,以便此类的客户端可以从实例的调用列表中添加和删除方法。 编译器强制添加或删除方法的签名与声明委托类型时使用的签名匹配。

声明委托的实例

定义委托类型后,可以创建该类型的实例(变量)。 将此视为创建一个“槽”,你可以在其中存储对方法的引用。

与 C# 中的所有变量一样,不能直接在命名空间或全局命名空间中声明委托实例。

// Inside a class definition:
public Comparison<T> comparator;

此变量的类型是 Comparison<T> (前面定义的委托类型),变量的名称为 comparator。 此时, comparator 还没有指向任何方法,就像等待填充的空槽一样。

还可以将委托变量声明为局部变量或方法参数,就像任何其他变量类型一样。

调用委托

具有指向方法的委托实例后,你就可以通过委托调用该方法。 就像调用方法一样,你可通过调用某个委托来调用处于该委托调用列表中的方法。

下面是该方法如何使用 Sort() 比较委托来确定对象的顺序:

int result = comparator(left, right);

在此行中,代码会调用附加到委托的方法。 将委托变量视为方法名称,并使用普通方法调用语法调用它。

但是,这行代码做出了一个不安全的假设:它假定目标方法已添加到委托对象中。 如果未附加任何方法,则上面的行会导致抛出 NullReferenceException。 用于解决此问题的模式比简单的 null 检查更为复杂,稍后将在本 系列中介绍。

分配、添加和删除调用目标

现在,你已了解如何定义委托类型、声明委托实例和调用委托。 但是,实际上如何将方法连接到委托呢? 这就是委托分配发挥作用的地方。

若要使用委托,你需要为其分配方法。 分配的方法必须与委托类型所定义的签名(参数和返回类型相同)。

让我们看看一个实际示例。 假设你想要按字符串长度对字符串列表进行排序。 需要创建一个与委托签名匹配的 Comparison<string> 比较方法。

private static int CompareLength(string left, string right) =>
    left.Length.CompareTo(right.Length);

此方法采用两个字符串并返回一个整数,指示哪个字符串“更大”(在本例中更长)。 该方法声明为私有方法,这是完全可以的。 你不需要将方法作为公共接口的一部分以将其与委托一起使用。

现在,你可以将这个方法传递给 List.Sort() 方法。

phrases.Sort(CompareLength);

请注意,使用方法名称时不带括号。 这告知编译器将方法引用转换为以后可以调用的委托。 每当需要比较两个字符串时,该方法 Sort() 都会调用方法 CompareLength

还可以通过声明委托变量并向其分配方法来更明确:

Comparison<string> comparer = CompareLength;
phrases.Sort(comparer);

这两种方法都实现了相同的目的。 第一种方法更简洁,而第二种方法使委托分配更加明确。

对于简单方法,通常使用 lambda 表达式 ,而不是定义单独的方法:

Comparison<string> comparer = (left, right) => left.Length.CompareTo(right.Length);
phrases.Sort(comparer);

Lambda 表达式提供了一种紧凑的方式来定义简单的内联方法。 后续部分中更详细地介绍了如何对委托目标使用 lambda 表达式。

到目前为止,这些示例显示具有单个目标方法的委托。 但是,委托对象可以支持将多个目标方法附加到单个委托对象的调用列表。 此功能对于事件处理方案特别有用。

委托和 MulticastDelegate 类

在后台,你一直在使用的委托功能建立在 .NET Framework 中的两个关键类上: DelegateMulticastDelegate。 你通常不会直接使用这些类,但它们提供了使委托工作的基础。

System.Delegate 类及其直接子类 System.MulticastDelegate 为创建委托、将方法注册为委托目标以及调用向委托注册的所有方法提供框架支持。

下面是一个有趣的设计详细信息:System.DelegateSystem.MulticastDelegate 并不是你可以使用的委托类型。 相反,它们充当所创建的所有特定委托类型的基类。 C# 语言会阻止你直接从这些类继承—你必须改用 delegate 关键字。

使用 delegate 关键字声明委托类型时,C# 编译器会根据您的特定签名,自动创建一个从 MulticastDelegate 派生的类。

为什么这种设计?

此设计源于 C# 和 .NET 的第一个版本。 设计团队有几个目标:

  1. 类型安全性:团队希望在使用委托时确保语言强制实施类型安全性。 这意味着确保使用正确的类型和参数数调用委托,并在编译时正确验证返回类型。

  2. 性能:通过让编译器生成表示特定方法签名的具体委托类,运行时可以优化委托调用。

  3. 简单性:委托包含在 1.0 .NET 版本中,这是在引入泛型之前。 在时间限制内工作所需的设计。

解决方案是让编译器创建与方法签名匹配的具体委托类,确保在隐藏复杂性的同时确保类型安全性。

使用委托方法

尽管您无法直接创建派生类,您偶尔也会使用在Delegate类和MulticastDelegate类上所定义的方法。 以下是要了解的最重要事项:

你使用的每个委托都派生自 MulticastDelegate。 “多播”委托意味着通过委托进行调用时,可以调用多个方法目标。 原始设计考虑区分只能调用一种方法的委托与可以调用多个方法的委托。 实践证明,这种区别并没有最初想象的那么有用,因此 .NET 中的所有委托都支持多种目标方法。

使用委托时最常用的方法是:

  • Invoke():调用委托中附加的所有方法
  • BeginInvoke() / EndInvoke():用于异步调用模式(虽然 async/await 现在首选)

在大多数情况下,不会直接调用这些方法。 相反,你将对委托变量使用方法调用语法,如上面的示例所示。 但是,正如 稍后在本系列中看到的那样,有一些模式可以直接使用这些方法。

概要

现在,你已经了解了 C# 语言语法如何映射到基础 .NET 类,接下来,你可以探索在更复杂的场景中如何使用、创建和调用强类型委托。

下一步