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# 编写 LINQ 时,还可以使用 lambda 表达式,如下例所示:
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 类中(例如,在 LINQ to Objects 和 LINQ to XML 中)调用 Enumerable.Select 方法,则参数为委托类型 System.Func<T,TResult>。 如果在 System.Linq.Queryable 类中(例如,在 LINQ to SQL 中)调用 Queryable.Select 方法,则参数类型为表达式树类型 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 表达式将计算那些除以 2 时余数为 1 的整数的数量 (n
)。
下面的示例生成一个序列,其中包含 numbers
数组中位于 9 之前的所有元素,因为这是序列中第一个不符合条件的数字:
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 类型。
从 C# 10 开始,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
从 C# 10 开始,可以在输入参数前面指定 Lambda 表达式的返回类型。 指定显式返回类型时,必须将输入参数括起来:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
属性
从 C# 10 开始,可以将属性添加到 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 表达式块之外,Lambda 表达式不得包含 goto、break 或 continue 语句。 同样,如果目标在块内部,在 lambda 表达式块外部使用跳转语句也是错误的。
可以将 static
修饰符应用于 Lambda 表达式,来防止 Lambda 无意中捕获本地变量或实例状态:
Func<double, double> square = static x => x * x;
静态 lambda 无法从封闭范围中捕获本地变量或实例状态,但可以引用静态成员和常量定义。
C# 语言规范
有关详细信息,请参阅 C# 语言规范中的 匿名函数表达式部分。
有关这些功能的详细信息,请参阅以下功能建议说明: