C# 中匿名函数-Lambda 表达式
一、什么是匿名函数
匿名函数,简单说就是“没有名字的函数”。
平时我们写一个普通方法,通常会给它起名字:
static int Add(int a, int b)
{
return a + b;
}
这个方法的名字叫 Add,以后可以通过 Add(1, 2) 来调用。
而匿名函数没有方法名,它通常是“临时写出来,马上交给某个变量、参数或事件使用”的一段代码。
例如:
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(1, 2)); // 输出 3
这里的:
(a, b) => a + b
就是一个匿名函数。
它没有名字,但可以赋值给变量 add,之后通过 add(1, 2) 来调用。
二、匿名函数的两种主要写法
C# 中匿名函数主要有两种写法:
- 匿名方法:
delegate写法 - Lambda 表达式:
=>写法
现在实际开发中,最常用的是 Lambda 表达式。
1. 匿名方法:delegate 写法
匿名方法是 C# 2.0 引入的写法。
Action sayHello = delegate
{
Console.WriteLine("你好,C#");
};
sayHello();
如果有参数,也可以这样写:
Action<string> sayHello = delegate (string name)
{
Console.WriteLine("你好," + name);
};
sayHello("小明");
这段代码中:
delegate (string name)
{
Console.WriteLine("你好," + name);
}
就是匿名方法。
2. Lambda 表达式:=> 写法
Lambda 表达式是 C# 3.0 引入的写法,也是现在最常见的匿名函数写法。
Action<string> sayHello = name =>
{
Console.WriteLine("你好," + name);
};
sayHello("小明");
如果函数体只有一句表达式,还可以写得更简洁:
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(10, 20)); // 输出 30
这里的 => 可以读作“变成”或“执行后得到”。
(a, b) => a + b
可以理解为:
传入
a和b,返回a + b的结果。
三、匿名函数通常要配合委托使用
匿名函数本身不能孤零零地存在,它通常需要赋值给一种“可以表示方法的类型”。
在 C# 中,这种类型叫做委托。
常见的委托类型有:
| 委托类型 | 说明 |
|---|---|
Action |
表示没有返回值的方法 |
Action<T> |
表示有参数、没有返回值的方法 |
Func<TResult> |
表示没有参数、有返回值的方法 |
Func<T, TResult> |
表示有参数、有返回值的方法 |
Predicate<T> |
表示判断条件,返回 bool |
1. Action:没有返回值
Action 表示一个没有返回值的方法。
Action print = () =>
{
Console.WriteLine("这是一段没有返回值的代码");
};
print();
如果有一个参数:
Action<string> printName = name =>
{
Console.WriteLine("姓名:" + name);
};
printName("张三");
如果有多个参数:
Action<string, int> printInfo = (name, age) =>
{
Console.WriteLine($"{name} 今年 {age} 岁");
};
printInfo("张三", 18);
2. Func:有返回值
Func 表示一个有返回值的方法。
Func<int> getNumber = () =>
{
return 100;
};
Console.WriteLine(getNumber()); // 输出 100
有参数、有返回值:
Func<int, int, int> add = (a, b) =>
{
return a + b;
};
Console.WriteLine(add(3, 5)); // 输出 8
如果函数体只有一个表达式,可以省略 {} 和 return:
Func<int, int, int> add = (a, b) => a + b;
注意:
Func<int, int, int>
前两个 int 是参数类型,最后一个 int 是返回值类型。
也就是说:
Func<参数1类型, 参数2类型, 返回值类型>
3. Predicate:返回 bool 的判断函数
Predicate<T> 常用于判断某个对象是否满足条件。
Predicate<int> isEven = number => number % 2 == 0;
Console.WriteLine(isEven(10)); // True
Console.WriteLine(isEven(11)); // False
它等价于:
Func<int, bool> isEven = number => number % 2 == 0;
四、Lambda 表达式的常见写法
1. 没有参数
没有参数时,必须写空括号 ()。
Action hello = () => Console.WriteLine("Hello");
不能写成:
// 错误
Action hello = => Console.WriteLine("Hello");
2. 一个参数
一个参数时,可以省略小括号:
Action<string> print = name => Console.WriteLine(name);
也可以不省略:
Action<string> print = (name) => Console.WriteLine(name);
3. 多个参数
多个参数时,必须写小括号:
Func<int, int, int> add = (a, b) => a + b;
4. 显式写参数类型
多数时候,C# 可以自动推断参数类型:
Func<int, int, int> add = (a, b) => a + b;
也可以显式写出来:
Func<int, int, int> add = (int a, int b) => a + b;
显式写类型可以让代码更清楚,但有时会显得啰嗦。
5. 表达式 Lambda
函数体只有一个表达式时,可以写成表达式 Lambda:
Func<int, int> square = x => x * x;
这表示:
int Square(int x)
{
return x * x;
}
6. 语句 Lambda
函数体有多行代码时,需要使用 {}:
Func<int, int> square = x =>
{
int result = x * x;
return result;
};
如果委托有返回值,在 {} 里面通常需要写 return。
五、匿名函数的常见使用场景
1. 简化临时代码
如果某段逻辑只用一次,没有必要单独写一个命名方法。
普通方法写法:
static bool IsAdult(int age)
{
return age >= 18;
}
匿名函数写法:
Func<int, bool> isAdult = age => age >= 18;
Console.WriteLine(isAdult(20)); // True
这种写法适合短小、明确、局部使用的逻辑。
2. 用在 LINQ 查询中
匿名函数在 LINQ 中非常常见。
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
foreach (var n in evenNumbers)
{
Console.WriteLine(n);
}
这里的:
n => n % 2 == 0
表示筛选条件:只保留偶数。
再看一个对象列表的例子:
class Student
{
public string Name { get; set; }
public int Score { get; set; }
}
List<Student> students = new List<Student>
{
new Student { Name = "小明", Score = 90 },
new Student { Name = "小红", Score = 75 },
new Student { Name = "小刚", Score = 60 }
};
var highScoreStudents = students.Where(s => s.Score >= 80);
foreach (var student in highScoreStudents)
{
Console.WriteLine(student.Name);
}
这里的:
s => s.Score >= 80
表示筛选出成绩大于等于 80 的学生。
3. 用在排序中
匿名函数经常用来告诉程序“按照什么规则排序”。
List<int> numbers = new List<int> { 5, 2, 8, 1 };
numbers.Sort((a, b) => a.CompareTo(b));
foreach (var n in numbers)
{
Console.WriteLine(n);
}
从大到小排序:
numbers.Sort((a, b) => b.CompareTo(a));
4. 用在事件中
按钮点击、定时器触发等事件中,也经常使用匿名函数。
例如 WinForms 中:
button1.Click += (sender, e) =>
{
MessageBox.Show("按钮被点击了");
};
这表示:
当按钮被点击时,执行
{}里面的代码。
如果用普通方法写,可能是这样:
button1.Click += Button1_Click;
private void Button1_Click(object sender, EventArgs e)
{
MessageBox.Show("按钮被点击了");
}
匿名函数适合逻辑很短、只在这里使用的事件处理。
5. 用作回调函数
回调函数就是“把一段代码交给别人,等合适的时候再执行”。
static void DoWork(Action callback)
{
Console.WriteLine("正在执行任务...");
callback();
}
DoWork(() =>
{
Console.WriteLine("任务完成后的操作");
});
这里的匿名函数:
() =>
{
Console.WriteLine("任务完成后的操作");
}
会被传给 DoWork 方法,等任务执行完后再调用。
六、匿名函数可以访问外部变量:闭包
匿名函数可以访问它外面的局部变量,这种现象叫闭包。
int count = 0;
Action increase = () =>
{
count++;
Console.WriteLine(count);
};
increase(); // 1
increase(); // 2
increase(); // 3
匿名函数内部访问了外部变量 count。
这很方便,但也容易出错。
闭包的重点理解
匿名函数捕获的不是变量当时的值,而是变量本身。
看下面的例子:
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action();
}
很多初学者会以为输出:
0
1
2
但在一些场景中,可能会因为变量捕获导致结果不是自己预期的。
为了写得更稳妥,可以在循环内部创建一个临时变量:
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int current = i;
actions.Add(() => Console.WriteLine(current));
}
foreach (var action in actions)
{
action();
}
这样每个匿名函数捕获的是各自的 current,更容易符合预期。
教学时可以这样解释:
匿名函数像是把外面的变量“记住了”,但它记住的是变量这个盒子,不一定只是盒子里当时的值。
七、匿名函数和普通方法的对比
| 对比项 | 普通方法 | 匿名函数 |
|---|---|---|
| 是否有名字 | 有 | 没有 |
| 适合场景 | 逻辑较复杂、需要复用 | 逻辑较短、临时使用 |
| 可读性 | 名字能表达含义 | 过长时可读性下降 |
| 使用位置 | 通常写在类中 | 可以直接写在变量、参数、事件中 |
| 是否能访问外部局部变量 | 通常不能直接访问 | 可以捕获外部变量 |
普通方法示例:
static bool IsEven(int number)
{
return number % 2 == 0;
}
var result = numbers.Where(IsEven);
匿名函数示例:
var result = numbers.Where(number => number % 2 == 0);
如果逻辑短,用匿名函数会更简洁。
如果逻辑长、业务含义强,建议写成普通方法,并起一个清楚的方法名。
八、匿名函数的注意事项
1. 不要把匿名函数写得太长
匿名函数适合短小逻辑。
不推荐:
button1.Click += (sender, e) =>
{
// 这里写了几十行代码
// 又查数据库,又处理文件,又更新界面
// 代码会越来越难读
};
更推荐:
button1.Click += (sender, e) =>
{
SaveUserInfo();
};
或者直接:
button1.Click += Button1_Click;
原则:
匿名函数越短越好,复杂逻辑应该提取成有名字的方法。
2. 注意参数类型和返回值类型要匹配
例如:
Func<int, int> square = x => x * x;
表示传入一个 int,返回一个 int。
下面这样就不匹配:
// 错误:Func<int, int> 要求返回 int,但这里返回 string
Func<int, int> wrong = x => "结果是:" + x;
如果要返回字符串,应该改成:
Func<int, string> right = x => "结果是:" + x;
3. 有返回值时,不要忘记 return
表达式 Lambda 可以自动返回结果:
Func<int, int> square = x => x * x;
但语句 Lambda 需要显式写 return:
Func<int, int> square = x =>
{
return x * x;
};
下面这样是错误的:
// 错误:有大括号时,需要 return
Func<int, int> square = x =>
{
x * x;
};
4. 注意闭包带来的变量捕获问题
匿名函数可以访问外部变量,但要小心变量在之后发生变化。
int number = 10;
Func<int> getNumber = () => number;
number = 20;
Console.WriteLine(getNumber()); // 输出 20
很多人会以为输出 10,但实际输出 20。
原因是匿名函数捕获的是变量 number 本身,而不是创建匿名函数那一刻的值。
如果希望固定当时的值,可以复制一份:
int number = 10;
int snapshot = number;
Func<int> getNumber = () => snapshot;
number = 20;
Console.WriteLine(getNumber()); // 输出 10
5. 事件中的匿名函数不容易取消订阅
事件订阅时可以使用匿名函数:
button1.Click += (sender, e) =>
{
Console.WriteLine("点击了按钮");
};
但是如果之后想取消订阅,会比较麻烦。
下面这种写法不能取消上面的订阅:
button1.Click -= (sender, e) =>
{
Console.WriteLine("点击了按钮");
};
因为这两个匿名函数看起来一样,但其实是两个不同的函数对象。
如果需要取消订阅,应该先保存起来:
EventHandler handler = (sender, e) =>
{
Console.WriteLine("点击了按钮");
};
button1.Click += handler;
// 需要取消时
button1.Click -= handler;
6. 不要滥用匿名函数影响可读性
下面代码虽然可以运行,但不适合教学和维护:
var result = students
.Where(s => s.Score > 60 && s.Name.StartsWith("小") && s.Name.Length > 1)
.OrderByDescending(s => s.Score)
.Select(s => new { s.Name, Level = s.Score >= 90 ? "优秀" : "合格" });
如果规则变复杂,可以拆成有名字的方法:
static bool IsQualifiedStudent(Student student)
{
return student.Score > 60
&& student.Name.StartsWith("小")
&& student.Name.Length > 1;
}
var result = students.Where(IsQualifiedStudent);
有名字的方法能让代码更容易读,也更方便调试。
7. 异步匿名函数要配合 async 和 await
匿名函数也可以写成异步的:
Func<Task> loadDataAsync = async () =>
{
await Task.Delay(1000);
Console.WriteLine("数据加载完成");
};
await loadDataAsync();
如果有参数:
Func<string, Task> downloadAsync = async url =>
{
await Task.Delay(1000);
Console.WriteLine("下载完成:" + url);
};
await downloadAsync("https://example.com");
注意:
async匿名函数通常返回Task或Task<T>。- 尽量避免
async void,除非是在事件处理器中。
事件处理器中常见写法:
button1.Click += async (sender, e) =>
{
await Task.Delay(1000);
MessageBox.Show("操作完成");
};
8. 表达式树中的 Lambda 不等于普通委托
在一些高级场景中,Lambda 不只是代表一段可以执行的代码,还可以被转换成表达式树。
例如:
Expression<Func<int, bool>> expression = x => x > 10;
这里的 x => x > 10 不是直接拿来执行的普通函数,而是被保存成一种“代码结构”。
这个知识常见于 Entity Framework、LINQ to SQL 等框架中。
初学阶段只需要知道:
普通
Func是可以直接执行的函数;Expression<Func<...>>更像是把代码保存成数据,让框架分析。
九、课堂上可以这样讲
可以把匿名函数理解成:
一张临时小纸条,上面写着一段要执行的代码。
这张纸条可以交给变量、方法或事件,等需要的时候再执行。
例如:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = numbers.Where(n => n > 3);
这里的:
n => n > 3
就像告诉 Where:
你帮我遍历每个数字,每次把数字放到
n里,只留下大于 3 的。
所以 Where 最后得到:
4
5
十、完整示例:用匿名函数处理学生成绩
using System;
using System.Collections.Generic;
using System.Linq;
class Student
{
public string Name { get; set; }
public int Score { get; set; }
}
class Program
{
static void Main()
{
List<Student> students = new List<Student>
{
new Student { Name = "小明", Score = 95 },
new Student { Name = "小红", Score = 82 },
new Student { Name = "小刚", Score = 59 },
new Student { Name = "小丽", Score = 76 }
};
// 筛选及格学生
var passedStudents = students.Where(s => s.Score >= 60);
// 按成绩从高到低排序
var orderedStudents = passedStudents.OrderByDescending(s => s.Score);
// 输出结果
foreach (var student in orderedStudents)
{
Console.WriteLine($"{student.Name}:{student.Score}");
}
}
}
程序输出:
小明:95
小红:82
小丽:76
这个例子里使用了两个匿名函数:
s => s.Score >= 60
表示筛选及格学生。
s => s.Score
表示排序时使用学生成绩作为排序依据。
十一、练习题
练习 1:判断奇数
请使用 Func<int, bool> 写一个匿名函数,判断一个整数是否是奇数。
参考答案:
Func<int, bool> isOdd = n => n % 2 != 0;
Console.WriteLine(isOdd(3)); // True
Console.WriteLine(isOdd(4)); // False
练习 2:字符串长度
请使用匿名函数返回字符串的长度。
参考答案:
Func<string, int> getLength = text => text.Length;
Console.WriteLine(getLength("CSharp")); // 6
练习 3:筛选高分学生
从学生列表中筛选出成绩大于等于 90 的学生。
参考答案:
var excellentStudents = students.Where(s => s.Score >= 90);
练习 4:点击事件
使用匿名函数给按钮添加点击事件,点击后弹出提示。
参考答案:
button1.Click += (sender, e) =>
{
MessageBox.Show("你好,匿名函数");
};
十二、总结
匿名函数是 C# 中非常重要的语法,尤其在 LINQ、事件、回调、异步编程中经常出现。
可以记住下面几句话:
- 匿名函数就是没有名字的函数。
- 它通常赋值给
Action、Func、Predicate或传给方法参数。 =>是 Lambda 表达式的核心符号。- 没有返回值用
Action,有返回值用Func。 - 匿名函数适合短小、临时、局部使用的逻辑。
- 复杂逻辑应提取成普通方法,提高可读性。
- 匿名函数可以访问外部变量,但要注意闭包问题。
- 事件中使用匿名函数时,如果以后要取消订阅,应先把它保存到变量中。
一句话概括:
匿名函数让我们可以把“行为”当作数据一样传来传去,从而写出更灵活、更简洁的 C# 代码。