C# 中 Lambda 表达式的全面讲解

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# 提供了多种内置委托类型,其中最常用的是 FuncAction

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 表达式的基本语法和形式
  • FuncAction 委托的结合使用
  • 在 LINQ 查询中的核心应用
  • 表达式树的概念与用途
  • 变量捕获(闭包)机制及注意事项
  • 异步 Lambda 等高级用法
  • 最佳实践建议

掌握 Lambda 表达式不仅能让我们写出更简洁、优雅的代码,更是深入理解 LINQ、EF Core 等现代 C# 技术栈的基石。建议在日常开发中多加练习,逐步培养使用 Lambda 表达式的思维习惯。

相关推荐
...6222 小时前
WinForm中Socket通信服务端客户端代码实现讲解
tcp/ip·c#·socket
唐青枫19 小时前
别只会看日志:C#.NET SOS 调试扩展从 Dump 到内存泄漏排查
c#·.net
yswenli1 天前
基于C#开发网站视频嗅探下载工具——BrowserVideoGrabber
c#·videodownload·webvideodownload·videograbber
Lost of 程序猿1 天前
M2 实战:询价与采购订单聚合落地——跨聚合一致性、领域事件与 Outbox
开发语言·c#·asp.net·.net
Polevne1 天前
C# GRPC 一元与双向流
linux·算法·c#
XiaoZhenHua982 天前
C#工业机器视觉Recipe配方系统设计:多型号、多相机、多工位参数如何统一管理
c#·软件架构·机器视觉·工业自动化·工业软件·recipe
fujisheng6612 天前
FUI 导航实践:拆开 Layer、History、Coverage 与 Cache 的组合语义
c#·unity3d
淡海水2 天前
10-05-高级-模式匹配-CSharp如何改变与数据结构的交互方式
数据结构·算法·c#·模式匹配
XiaoZhenHua982 天前
C# WinForms + 西门子S7 + SQLite + EPPlus生产数据采集系统架构设计
系统架构·sqlite·c#