Lambda 表达式、委托和事件

小窍门

开发软件的新手? 首先开始 学习入门 教程。 在使用 lambda 表达式之前,先在此处生成核心类型和方法技能。

有时,你想要将一小部分行为(函数)直接传递给另一种方法。 例如,你可能想要筛选列表,但筛选条件会根据情况而更改。 而不是为每个可能的条件编写单独的命名方法,而是将条件本身作为参数传递。

Lambda 表达式 是 C# 功能,使此功能成为可能。 lambda 表达式是一个精简的内联函数,无需为其指定名称即可编写。 使用箭头运算符 => 将参数列表与正文分开:

x => x * 2

从左到右读取: x 是输入参数, => 表示“转到”,并且 x * 2 是正文。 它计算返回的值。 如果没有参数或多个参数,请将它们括在括号中: () => 42(left, right) => left + right

委托支持 lambda 表达式

若要使用 lambda 表达式,C# 编译器需要知道两件事:参数的类型和返回类型。 该说明(参数类型加上返回类型)称为 委托类型

委托类型是表示方法签名的类型。 委托类型的变量可以保留任何匹配的方法,如 lambda 表达式或命名方法,只要其参数类型和返回类型匹配。

声明委托类型时使用关键字delegate

delegate int Transform(int value);

此声明说:“Transform 是接受一个 int 并返回方法 int的委托类型。然后,可以将 lambda 表达式或命名方法分配给该类型的变量:

Transform doubler = x => x * 2;    // assign a lambda expression
Transform squarer = Square;         // assign a named method

Console.WriteLine(doubler(5));      // 10
Console.WriteLine(squarer(5));      // 25

static int Square(int value) => value * value;

doublersquarer 均为 Transform 类型的值。 可采用与常规方法完全相同的方式调用它们。 编译器验证你分配的任何项是否与声明的签名匹配。

内置委托类型: FuncAction

为每种情况声明自定义委托类型可能是一项重复工作。 .NET提供了两个泛型委托类型(FuncAction)系列,这些类型涵盖大多数方案,因此你很少需要自己使用 delegate 关键字。

这两个系列都有零到 16 个输入类型参数的版本,因此它们可扩展到任意数量的输入。 两个家庭之间的主要区别是:

  • System.Func<T,TResult> Func<T1, T2, TResult>(等等)表示返回值的方法。 最后一个类型参数始终为返回类型;所有以前的都是输入类型。
  • System.Action<T> (等等 Action<T1, T2>)表示 不返回任何void) 的方法。 所有类型参数都是输入类型。 System.Action 没有类型参数表示没有输入和返回值的方法。

例如, Func<int, int, int> 描述具有两 int 个输入和一个 int 结果的方法。 Action<string> 描述具有一个 string 输入且无返回值的方法。

Func<int, int, int> add = (left, right) => left + right;
Action<string> report = message => Console.WriteLine($"Report: {message}");

int total = add(5, 9);
report($"5 + 9 = {total}");

在 lambda 中使用描述性参数名称,以便读者无需扫描完整的方法正文即可理解意向。

将 lambda 表达式传递给方法

当方法声明 FuncAction 参数时,调用方会传递与相应委托类型匹配的 lambda 表达式。 编译器检查 lambda 的参数类型和返回类型是否与声明的委托类型匹配。 如果它们不匹配,则代码不会编译。

int[] numbers = [1, 2, 3, 4, 5, 6];
int[] evenNumbers = Filter(numbers, value => value % 2 == 0).ToArray();

Console.WriteLine(string.Join(", ", evenNumbers));

Filter 方法声明一个名为 predicateFunc<int, bool> 参数。 该 Func<int, bool> 类型告知调用方预期的形状:一个 int 输入,一个 bool 结果。 调用方将 value => value % 2 == 0 作为参数传递。 此模式在整个 LINQ 和许多.NET API 中出现。

使 lambda 表达式保持自包含

lambda 表达式可以引用周围代码中的变量。 捕获意味着 lambda 会保存在其自身主体外声明的变量的引用。 lambda 及其捕获的变量的组合称为 闭包

如果不需要捕获任何内容,请将 static 修饰符添加到 lambda。 静态 lambda 只能使用在其主体中声明的自身参数和值。 它无法从封闭范围捕获局部变量或实例状态。

Func<int, bool> isEven = static value => value % 2 == 0;

Console.WriteLine(isEven(14));
Console.WriteLine(isEven(15));

静态 lambda 将明确意向并防止意外捕获。

当输入不相关时使用忽略参数

有时委托签名包含不需要的参数。 使用 discard _ 明确表示该选择。

常见示例包括不使用 senderEventArgs 的事件处理程序,只需多项输入中的几项的回调,以及提供未使用的索引的 LINQ 重载。

Action<int, int, string> statusUpdate = (_, _, message) => Console.WriteLine(message);
statusUpdate(200, 42, "Operation completed");

弃用项可提高可读性,因为它们展示了哪些参数很重要。

事件提供可选通知

事件是一种机制,一个对象(发布者)用于在发生某种情况时通知其他对象(订阅者)。 发布者不需要知道谁正在侦听,也不需要知道有多少订阅者。 订阅者选择加入。

事件在委托的基础上构建。 事件是一个具有由 event 关键字施加额外限制的委托字段:外部代码只能对事件进行订阅(+=)或取消订阅(-=);只有声明事件的类可以调用(触发)事件。

事件委托类型的.NET约定为 System.EventHandler<TEventArgs>,其中T是通知中包含的数据类型。 其签名始终具有两个参数: sender (引发事件的对象)和类型的 T事件数据。

MessagePublisher publisher = new();
publisher.MessagePublished += (_, message) => Console.WriteLine($"Received: {message}");

publisher.Publish("Records updated");

演练代码:

  • MessagePublisher 声明 event EventHandler<string>? MessagePublished。 关键字 event 意味着调用方只能订阅或取消订阅,它们无法直接调用它。
  • publisher.MessagePublished += (_, message) => ... 使用 lambda 表达式订阅。 _ 参数被丢弃,因为此处理程序不需要它。
  • publisher.Publish("Records updated") 引发事件并运行每个订阅的处理程序。

订阅是可选的。 方法 Publish 中的 ?.Invoke(...) 意味着仅当附加至少一个订阅程序时才会引发事件。 发布者在不知情或不关心是否有人在侦听的情况下引发事件。

另见