Lambda 表达式和匿名函数
使用 lambda 表达式 创建匿名函数。 使用 lambda 声明运算符 =>
将 lambda 的参数列表与其正文分开。 lambda 表达式可以是以下两种形式中的任何一种:
以表达式为主体的表达式 lambda:
(input-parameters) => expression
语句块作为其主体的语句 lambda:
(input-parameters) => { <sequence-of-statements> }
若要创建 lambda 表达式,请指定 lambda 运算符左侧的输入参数(如果有)和另一端的表达式或语句块。
任何 Lambda 表达式都可以转换为委托类型。 其参数的类型和返回值定义 lambda 表达式可转换为的委托类型。 如果 lambda 表达式未返回值,则可以将其转换为 Action
委托类型之一;否则,它可以转换为 Func
委托类型之一。 例如,有 2 个参数且不返回值的 Lambda 表达式可转换为 Action<T1,T2> 委托。 有 1 个参数且不返回值的 Lambda 表达式可转换为 Func<T,TResult> 委托。 以下示例中,Lambda 表达式 x => x * x
(指定名为 x
的参数并返回 x
平方值)将分配给委托类型的变量:
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25
表达式 lambda 还可以转换为表达式树类型,如下面的示例所示:
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)
您可以在任何需要委托类型或表达式树实例的代码中使用 lambda 表达式。 一个示例是 Task.Run(Action) 方法的参数,用于传递应在后台执行的代码。 在 C#中编写
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25
使用基于方法的语法调用 System.Linq.Enumerable 类中的 Enumerable.Select 方法(例如 LINQ to Objects 和 LINQ to XML 中)时,参数是委托类型 System.Func<T,TResult>。 在 System.Linq.Queryable 类中调用 Queryable.Select 方法(例如在 LINQ to SQL 中)时,参数类型是表达式树类型 Expression<Func<TSource,TResult>>
。 在这两种情况下,都可以使用相同的 lambda 表达式来指定参数值。 尽管通过 Lambda 创建的对象实际具有不同的类型,但其使得 2 个 Select
调用看起来类似。
表达式 lambda
表达式位于 =>
运算符右侧的 lambda 表达式称为“表达式 lambda”。 表达式 lambda 返回表达式的结果,并采用以下基本形式:
(input-parameters) => expression
表达式 lambda 的主体可以包含方法调用。 不过,若要创建在 .NET 公共语言运行时 (CLR) 的上下文之外(如在 SQL Server 中)计算的表达式树,则不得在 Lambda 表达式中使用方法调用。 这些方法在 .NET 公共语言运行时(CLR)的上下文之外没有意义。
语句 lambda
语句 lambda 与表达式 lambda 类似,只是语句括在大括号中:
(input-parameters) => { <sequence-of-statements> }
语句 lambda 的正文可以包含任意数量的语句;但是,在实践中通常不超过两到三个。
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!
不能使用语句 lambda 来创建表达式树。
lambda 表达式的输入参数
将 lambda 表达式的输入参数括在括号中。 使用空括号指定零个输入参数:
Action line = () => Console.WriteLine();
如果 lambda 表达式只有一个输入参数,则括号是可选的:
Func<double, double> cube = x => x * x * x;
两个或多个输入参数用逗号分隔:
Func<int, int, bool> testForEquality = (x, y) => x == y;
有时编译器无法推断输入参数的类型。 可以显式指定类型,如以下示例所示:
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
输入参数类型必须为所有显式或全部隐式;否则,会发生 CS0748 编译器错误。
可以使用 忽略 来指定 lambda 表达式中不使用的两个或多个输入参数。
Func<int, int, int> constant = (_, _) => 42;
使用 Lambda 表达式提供事件处理程序时,Lambda 弃元参数可能很有用。
注意
为了向后兼容,如果只有单个输入参数命名为 _
,则在 lambda 表达式中,_
被视为该参数的名称。
从 C# 12 开始,可以为 lambda 表达式上的参数提供 默认值。 默认参数值的语法和限制与方法和本地函数的语法和限制相同。 以下示例使用默认参数声明 lambda 表达式,然后使用默认值调用一次,并使用两个显式参数调用一次:
var IncrementBy = (int source, int increment = 1) => source + increment;
Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7
还可以将具有 params
数组或集合的 lambda 表达式声明为参数:
var sum = (params IEnumerable<int> values) =>
{
int sum = 0;
foreach (var value in values)
sum += value;
return sum;
};
var empty = sum();
Console.WriteLine(empty); // 0
var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15
作为这些更新的一部分,当将具有默认参数的方法组分配给 lambda 表达式时,该 lambda 表达式也具有相同的默认参数。 还可以将具有 params
集合参数的方法组分配给 Lambda 表达式。
具有默认参数或 params
集合的 Lambda 表达式,因为参数没有与 Func<>
或 Action<>
类型对应的自然类型。 但是,可以定义包含默认参数值的委托类型:
delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);
或者,可以将隐式类型化变量与 var
声明一起使用来定义委托类型。 编译器合成正确的委托类型。
有关 lambda 表达式的默认参数的详细信息,请参阅 lambda 表达式
异步 lambda
通过使用 async 和 await 关键字,你可以轻松创建包含异步处理的 lambda 表达式和语句。 例如,下面的 Windows 窗体示例中有一个事件处理程序,它调用并等待一个异步方法,ExampleMethodAsync
。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
}
private async void button1_Click(object sender, EventArgs e)
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
你可以使用异步 lambda 添加同一事件处理程序。 若要添加此处理程序,请在 lambda 参数列表之前添加 async
修饰符,如以下示例所示:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += async (sender, e) =>
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
};
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
有关如何创建和使用异步方法的详细信息,请参阅 使用 async 和 await 的异步编程。
lambda 表达式和元组
C# 语言提供对元组的内置支持。 可以将元组作为 lambda 表达式的参数提供,lambda 表达式也可以返回元组。 在某些情况下,C# 编译器使用类型推理来确定元组组件的类型。
可通过用括号括住用逗号分隔的组件列表来定义元组。 以下示例使用包含三个组件的元组将数字序列传递给 lambda 表达式,该表达式将每个值加倍,并返回包含乘法结果的三个组件的元组。
Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)
通常,元组字段命名为 Item1
、Item2
等等。 但是,可以定义包含命名组件的元组,如以下示例所示。
Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
有关 C# 元组的详细信息,请参阅 元组类型。
含标准查询运算符的 lambda
在其他实现中,LINQ to Objects 有一个输入参数,其类型是泛型委托 Func<TResult> 系列中的一种。 这些委托使用类型参数来定义输入参数的数量和类型,以及委托的返回类型。 Func
委托对于封装用户定义的表达式很有用,这些表达式将应用于一组源数据中的每个元素。 例如,假设为 Func<T,TResult> 委托类型:
public delegate TResult Func<in T, out TResult>(T arg)
可以将委托实例化为 Func<int, bool>
实例,其中 int
是输入参数,bool
是返回值。 返回值始终在最后一个类型参数中指定。 例如,Func<int, string, bool>
定义包含两个输入参数(int
和 string
)且返回类型为 bool
的委托。 以下 Func
委托在调用时返回一个布尔值,该布尔值指示输入参数是否等于 5。
Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result); // False
也可以在参数类型为 Expression<TDelegate>时提供 lambda 表达式,例如,在 Queryable 类型中定义的标准查询运算符中。 指定 Expression<TDelegate> 参数时,lambda 将编译为表达式树。
以下示例使用 Count 标准查询运算符:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");
编译器可以推断输入参数的类型,也可以显式指定它。 此特定的 lambda 表达式计算那些整数(n
),这些整数被 2 除后余数为 1。
以下示例生成一个序列,其中包含位于 9 前面的 numbers
数组中的所有元素,因为这是序列中不符合条件的第一个数字:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3
以下示例通过将多个输入参数括在括号中来指定这些参数。 该方法将返回 numbers
数组中的所有元素,直到找到一个数字,其值小于数组中的序号位置:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4
不能直接在查询表达式中使用 lambda 表达式,但你可以在查询表达式的方法调用中使用它们,如以下示例所示:
var numberSets = new List<int[]>
{
new[] { 1, 2, 3, 4, 5 },
new[] { 0, 0, 0 },
new[] { 9, 8 },
new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};
var setsWithManyPositives =
from numberSet in numberSets
where numberSet.Count(n => n > 0) > 3
select numberSet;
foreach (var numberSet in setsWithManyPositives)
{
Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0
lambda 表达式中的类型推理
编写 lambda 时,通常不必为输入参数指定类型,因为编译器可以根据 lambda 正文、参数类型和 C# 语言规范中所述的其他因素推断类型。 对于大多数标准查询运算符,第一个输入是源序列中元素的类型。 如果要查询 IEnumerable<Customer>
,则输入变量被推断为 Customer
对象,这意味着你有权访问其方法和属性:
customers.Where(c => c.City == "London");
lambda 类型推理的一般规则如下所示:
- lambda 必须包含与委托类型相同的参数数。
- lambda 中的每个输入参数都必须隐式转换为其相应的委托参数。
- Lambda 的返回值(如果有)必须能够隐式转换为委托的返回类型。
lambda 表达式的自然类型
lambda 表达式本身没有类型,因为通用类型系统没有“lambda 表达式”的内部概念。但是,有时可以方便地非正式地谈论 lambda 表达式的“类型”。 该非正式“类型”是指委托类型或 Lambda 表达式所转换到的 Expression 类型。
lambda 表达式可以具有自然类型。 编译器不会强制你为 Lambda 表达式声明委托类型(例如 Func<...>
或 Action<...>
),而是根据 Lambda 表达式推断委托类型。 例如,请考虑以下声明:
var parse = (string s) => int.Parse(s);
编译器可以将 parse
推断为 Func<string, int>
。 编译器选择可用的 Func
或 Action
委托(如果存在合适的委托)。 否则,它将合成委托类型。 例如,如果 Lambda 表达式具有 ref
参数,则合成委托类型。 如果 lambda 表达式具有自然类型,则可以将其分配给不太明确的类型,例如 System.Object 或 System.Delegate:
object parse = (string s) => int.Parse(s); // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>
只有一个重载的方法组(即没有参数列表的方法名称)具有自然类型:
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
如果将 lambda 表达式分配给 System.Linq.Expressions.LambdaExpression或 System.Linq.Expressions.Expression,并且 lambda 具有自然委托类型,则表达式的自然类型为 System.Linq.Expressions.Expression<TDelegate>,自然委托类型作为类型参数的参数使用。
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
并非所有 lambda 表达式都具有自然类型。 请考虑以下声明:
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
编译器无法推断 s
的参数类型。 当编译器无法推断自然类型时,必须声明类型:
Func<string, int> parse = s => int.Parse(s);
显式返回类型
通常,lambda 表达式的返回类型是显而易见的,并且是推断的。 对于某些不起作用的表达式:
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
可以在输入参数之前指定 lambda 表达式的返回类型。 指定显式返回类型时,必须括号化输入参数:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
属性
可以将属性添加到 lambda 表达式及其参数。 以下示例演示如何向 lambda 表达式添加属性:
Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;
还可以将属性添加到输入参数或返回值,如以下示例所示:
var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;
如前面的示例所示,向 lambda 表达式或其参数添加属性时,必须括号化输入参数。
重要
Lambda 表达式通过基础委托类型调用。 这不同于方法和本地函数。 委托的 Invoke
方法不会检查 lambda 表达式的属性。 调用 lambda 表达式时,属性没有任何影响。 lambda 表达式上的属性对于代码分析非常有用,可以通过反射发现。 此决定的一个后果是,无法将 System.Diagnostics.ConditionalAttribute 应用于 lambda 表达式。
捕获 lambda 表达式中的外部变量和变量范围
lambda 可以引用外部变量。 外部变量是在定义 lambda 表达式的方法中或包含 lambda 表达式的类型中的范围内变量。 以这种方式捕获的变量将进行存储以备在 lambda 表达式中使用,即使在其他情况下,这些变量将超出范围并进行垃圾回收。 必须明确地分配外部变量,然后才能在 lambda 表达式中使用该变量。 以下示例演示了以下规则:
public static class VariableScopeWithLambdas
{
public class VariableCaptureGame
{
internal Action<int>? updateCapturedLocalVariable;
internal Func<int, bool>? isEqualToCapturedLocalVariable;
public void Run(int input)
{
int j = 0;
updateCapturedLocalVariable = x =>
{
j = x;
bool result = j > input;
Console.WriteLine($"{j} is greater than {input}: {result}");
};
isEqualToCapturedLocalVariable = x => x == j;
Console.WriteLine($"Local variable before lambda invocation: {j}");
updateCapturedLocalVariable(10);
Console.WriteLine($"Local variable after lambda invocation: {j}");
}
}
public static void Main()
{
var game = new VariableCaptureGame();
int gameInput = 5;
game.Run(gameInput);
int jTry = 10;
bool result = game.isEqualToCapturedLocalVariable!(jTry);
Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");
int anotherJ = 3;
game.updateCapturedLocalVariable!(anotherJ);
bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
}
// Output:
// Local variable before lambda invocation: 0
// 10 is greater than 5: True
// Local variable after lambda invocation: 10
// Captured local variable is equal to 10: True
// 3 is greater than 5: False
// Another lambda observes a new value of captured variable: True
}
以下规则适用于 lambda 表达式中的变量范围:
- 捕获的变量将不会被作为垃圾回收,直至引用变量的委托符合垃圾回收的条件。
- lambda 表达式中引入的变量在封闭方法中不可见。
- Lambda 表达式无法从封闭方法中直接捕获 in、ref 或 out 参数。
- lambda 表达式中的 return 语句不会导致封闭方法返回。
- lambda 表达式不能包含 goto、break或 continue 语句,如果这些跳转语句的目标在 lambda 表达式块之外。 同样,如果目标在块内部,在 lambda 表达式块外部使用跳转语句也是错误的。
可以将 static
修饰符应用于 lambda 表达式,以防止 lambda 无意中捕获局部变量或实例状态:
Func<double, double> square = static x => x * x;
静态 lambda 无法从封闭范围捕获局部变量或实例状态,但可以引用静态成员和常量定义。
C# 语言规范
有关详细信息,请参阅 C# 语言规范的 Anonymous 函数表达式 部分。
有关这些功能的详细信息,请参阅以下功能建议说明: