小窍门
开发软件的新手? 首先开始 学习入门 教程。 在使用 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;
doubler 和 squarer 均为 Transform 类型的值。 可采用与常规方法完全相同的方式调用它们。 编译器验证你分配的任何项是否与声明的签名匹配。
内置委托类型: FuncAction
为每种情况声明自定义委托类型可能是一项重复工作。 .NET提供了两个泛型委托类型(Func 和 Action)系列,这些类型涵盖大多数方案,因此你很少需要自己使用 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 表达式传递给方法
当方法声明 Func 或 Action 参数时,调用方会传递与相应委托类型匹配的 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 方法声明一个名为 predicate 的 Func<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 _ 明确表示该选择。
常见示例包括不使用 sender 或 EventArgs 的事件处理程序,只需多项输入中的几项的回调,以及提供未使用的索引的 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(...) 意味着仅当附加至少一个订阅程序时才会引发事件。 发布者在不知情或不关心是否有人在侦听的情况下引发事件。