1. 引言
Lambda 表达式是 C# 中非常强大且常用的语法特性,它让我们能够以简洁、优雅的方式编写匿名函数。无论是 LINQ 查询、事件处理、异步编程还是日常的集合操作,Lambda 表达式都扮演着不可或缺的角色。本文将带你从零开始,系统性地掌握 C# 中 Lambda 表达式的核心概念、语法、应用场景以及背后的原理。
2. 什么是 Lambda 表达式
Lambda 表达式是一种匿名函数,它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型。简单来说,Lambda 表达式就是"没有名字的方法",它允许我们在需要方法的地方直接内联定义逻辑,而无需单独声明一个具名方法。
2.1 为什么需要 Lambda 表达式
在 Lambda 表达式出现之前,我们通常需要编写完整的方法或使用匿名方法来传递逻辑。例如:
csharp
// 传统方式:定义一个具名方法
static bool IsEven(int number)
{
return number % 2 == 0;
}
// 使用方式
var evenNumbers = numbers.Where(IsEven);
而使用 Lambda 表达式,我们可以将上述逻辑内联:
csharp
var evenNumbers = numbers.Where(n => n % 2 == 0);
可以看到,Lambda 表达式让代码更加简洁、直观,尤其是在处理集合和 LINQ 查询时,能显著提升代码的可读性和开发效率。
3. Lambda 表达式的基本语法
Lambda 表达式的语法由三部分组成:输入参数、Lambda 运算符(=>)和表达式或语句块。
csharp
(输入参数) => 表达式或语句块
3.1 基本形式
csharp
// 形式一:表达式 Lambda(最常用)
x => x * x
// 形式二:语句 Lambda(包含多条语句)
x =>
{
var result = x * x;
return result;
};
// 形式三:带显式类型的参数
(int x) => x * x
// 形式四:无参数
() => Console.WriteLine("Hello")
// 形式五:多个参数
(x, y) => x + y
3.2 参数规则
- 当只有一个参数时,可以省略括号:
x => x * x - 当有多个参数或无参数时,必须使用括号:
(x, y) => x + y、() => DateTime.Now - 可以显式指定参数类型:
(int x, string s) => s.Length > x - 编译器通常可以根据上下文推断参数类型,因此大多数情况下无需显式声明
4. Lambda 表达式与委托
Lambda 表达式最常见的用途之一就是创建委托实例。C# 提供了多种内置委托类型,其中最常用的是 Func 和 Action。
4.1 Func 委托(有返回值)
csharp
// Func<T, TResult>:接受一个 T 类型参数,返回 TResult 类型结果
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // 输出:25
// Func<T1, T2, TResult>:接受两个参数
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 4)); // 输出:7
4.2 Action 委托(无返回值)
csharp
// Action<T>:接受一个参数,无返回值
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice"); // 输出:Hello, Alice!
// Action:无参数,无返回值
Action sayHello = () => Console.WriteLine("Hello!");
sayHello();
4.3 自定义委托
除了内置委托,Lambda 表达式也可以赋值给自定义委托类型:
csharp
public delegate int MathOperation(int a, int b);
MathOperation operation = (a, b) => a * b;
Console.WriteLine(operation(6, 7)); // 输出:42
5. Lambda 表达式在 LINQ 中的应用
Lambda 表达式是 LINQ 的灵魂。几乎所有 LINQ 扩展方法都接受 Lambda 表达式作为参数,这使得数据查询变得异常简洁。
5.1 常用 LINQ 操作
csharp
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Where:筛选
var evens = numbers.Where(n => n % 2 == 0);
// 结果:2, 4, 6, 8, 10
// Select:投影
var squares = numbers.Select(n => n * n);
// 结果:1, 4, 9, 16, 25, 36, 49, 64, 81, 100
// OrderBy:排序
var sorted = numbers.OrderByDescending(n => n);
// 结果:10, 9, 8, 7, 6, 5, 4, 3, 2, 1
// Any / All:判断
bool hasEven = numbers.Any(n => n % 2 == 0); // true
bool allPositive = numbers.All(n => n > 0); // true
// First / FirstOrDefault:取第一个
var firstEven = numbers.First(n => n % 2 == 0); // 2
5.2 对象集合查询
csharp
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
public string Major { get; set; }
}
var students = new List<Student>
{
new Student { Name = "张三", Age = 20, Major = "计算机" },
new Student { Name = "李四", Age = 22, Major = "数学" },
new Student { Name = "王五", Age = 21, Major = "计算机" }
};
// 查询计算机专业且年龄大于 20 的学生姓名
var result = students
.Where(s => s.Major == "计算机" && s.Age > 20)
.Select(s => s.Name);
// 按年龄分组
var grouped = students.GroupBy(s => s.Age);
6. Lambda 表达式与表达式树
Lambda 表达式不仅可以转换为委托,还可以转换为表达式树(Expression Tree)。表达式树将代码表示为数据结构,使得我们可以在运行时分析、修改和执行代码。这是 EF Core、LINQ to SQL 等 ORM 框架实现动态查询的基础。
csharp
using System.Linq.Expressions;
// 创建表达式树
Expression<Func<int, int>> expr = x => x * x + 1;
// 编译表达式树为委托
Func<int, int> func = expr.Compile();
Console.WriteLine(func(5)); // 输出:26
// 分析表达式树
var body = expr.Body; // 获取表达式主体
Console.WriteLine(body); // 输出:(x * x) + 1
6.1 委托与表达式树的区别
| 特性 | 委托(Delegate) | 表达式树(Expression Tree) |
|---|---|---|
| 本质 | 编译后的代码 | 数据结构(代码的描述) |
| 用途 | 直接执行 | 分析、转换、动态生成代码 |
| 典型场景 | LINQ to Objects | LINQ to SQL / EF Core |
| 性能 | 直接调用,性能高 | 需要编译,首次调用较慢 |
7. Lambda 表达式中的变量捕获
Lambda 表达式可以捕获其定义作用域中的变量,这被称为"闭包"(Closure)。被捕获的变量在 Lambda 执行时仍然有效。
csharp
int factor = 10;
Func<int, int> multiplier = x => x * factor;
Console.WriteLine(multiplier(5)); // 输出:50
// 修改捕获的变量
factor = 20;
Console.WriteLine(multiplier(5)); // 输出:100(使用最新的 factor 值)
7.1 闭包的注意事项
csharp
// 陷阱示例:在循环中使用 Lambda
var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action(); // 输出:5 5 5 5 5(而不是 0 1 2 3 4)
}
在 C# 5.0 之前,上述代码会输出 5 个 5,因为所有 Lambda 捕获的是同一个循环变量 i。从 C# 5.0 开始,foreach 循环中的迭代变量每次迭代都会创建新的变量,但 for 循环仍然存在此问题。解决方案是使用局部变量:
csharp
for (int i = 0; i < 5; i++)
{
int temp = i; // 创建局部副本
actions.Add(() => Console.WriteLine(temp));
}
8. Lambda 表达式的高级用法
8.1 异步 Lambda
csharp
// 异步 Lambda 表达式
Func<Task<int>> asyncLambda = async () =>
{
await Task.Delay(1000);
return 42;
};
// 在事件处理中使用异步 Lambda
button.Click += async (sender, e) =>
{
await Task.Delay(100);
Console.WriteLine("按钮被点击");
};
8.2 元组参数
csharp
// 使用元组作为 Lambda 参数
var tuples = new List<(int X, int Y)> { (1, 2), (3, 4), (5, 6) };
var sums = tuples.Select(t => t.X + t.Y);
// 结果:3, 7, 11
// 解构元组参数
var sumList = tuples.Select(((int X, int Y) t) => t.X + t.Y);
8.3 标准查询运算符中的 Lambda
csharp
var data = new[] { "apple", "banana", "cherry", "date" };
// 聚合操作
var totalLength = data.Sum(s => s.Length); // 22
// 分组
var byFirstChar = data.GroupBy(s => s[0]);
// 连接
var numbers = new[] { 1, 2, 3 };
var words = new[] { "one", "two", "three" };
var joined = numbers.Join(words, n => n, w => words.ToList().IndexOf(w) + 1, (n, w) => $"{n}: {w}");
9. Lambda 表达式的最佳实践
9.1 保持简洁
Lambda 表达式应该保持简短和清晰。如果逻辑过于复杂,建议提取为具名方法。
csharp
// 好的做法:简洁明了
var adults = people.Where(p => p.Age >= 18);
// 不好的做法:逻辑过于复杂
var complex = people.Where(p =>
{
if (p.Age >= 18 && p.IsActive)
{
return p.Score > 60 || p.HasRecommendation;
}
return false;
});
9.2 避免副作用
Lambda 表达式应尽量保持"纯净",即不修改外部状态。这有助于代码的可预测性和可测试性。
csharp
// 不好的做法:在 Lambda 中修改外部变量
int count = 0;
var selected = items.Where(item =>
{
count++; // 副作用
return item.IsValid;
});
// 好的做法:使用无副作用的 Lambda
var selected = items.Where(item => item.IsValid);
9.3 合理使用类型推断
大多数情况下,让编译器推断参数类型即可。只有在可读性受影响时才显式声明类型。
csharp
// 推荐:让编译器推断
var result = numbers.Select(n => n * 2);
// 仅在必要时显式声明
var result = numbers.Select((int n) => n * 2);
10. 总结
Lambda 表达式是 C# 中极具表现力的语法特性,它让函数式编程风格在 C# 中得以自然体现。通过本文的学习,我们掌握了:
- Lambda 表达式的基本语法和形式
- 与
Func、Action委托的结合使用 - 在 LINQ 查询中的核心应用
- 表达式树的概念与用途
- 变量捕获(闭包)机制及注意事项
- 异步 Lambda 等高级用法
- 最佳实践建议
掌握 Lambda 表达式不仅能让我们写出更简洁、优雅的代码,更是深入理解 LINQ、EF Core 等现代 C# 技术栈的基石。建议在日常开发中多加练习,逐步培养使用 Lambda 表达式的思维习惯。