C# 匿名方法
`delegate` 匿名方法、lambda 表达式、闭包捕获、以及陷阱。
Key Pointslambda 是匿名方法的新写法;闭包捕获的是**变量**,不是值。
两种写法
C#
// 匿名方法(C# 2.0)
Func<int, int> f1 = delegate (int x) { return x * 2; };
// lambda 语句体(C# 3.0)
Func<int, int> f2 = x => { return x * 2; };
// lambda 表达式体
Func<int, int> f3 = x => x * 2;
Console.WriteLine($"{f1(3)} {f2(3)} {f3(3)}");
// 无参数
Action a = () => Console.WriteLine("无参");
// 多参数
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(add(1, 2));Output
6 6 6
无参
3闭包捕获
C#
int counter = 0;
Action inc = () => counter++; // 捕获变量本身
inc();
inc();
inc();
Console.WriteLine(counter); // 3
// 捕获的是变量,不是当时的快照
int x = 10;
Action show = () => Console.WriteLine(x);
x = 20;
show(); // 20,不是 10Output
3
20Capturing the variable rather than its value is the classic trap. To capture a distinct value per iteration, declare a local inside the loop (
var local = i;). Since C# 5 foreach variables are per-iteration, but a for counter is shared.C#
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.Write(i + " ")); // 都捕获同一个 i
}
foreach (var a in actions) a();
Console.WriteLine();
var fixedActions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int local = i; // 每轮新变量
fixedActions.Add(() => Console.Write(local + " "));
}
foreach (var a in fixedActions) a();
Console.WriteLine();Output
3 3 3
0 1 2 表达式树
C#
using System.Linq.Expressions;
// 赋值给 Expression<T> 时,lambda 变成数据结构而不是代码
Expression<Func<int, bool>> expr = x => x > 5;
Console.WriteLine(expr.Body);
Console.WriteLine(expr.Parameters[0].Name);
// 可以分析它
if (expr.Body is BinaryExpression bin)
{
Console.WriteLine($"{bin.NodeType} {bin.Left} {bin.Right}");
}
// 也可以编译回委托
var func = expr.Compile();
Console.WriteLine(func(10));Output
(x > 5)
x
GreaterThan x 5
TrueExpression<Func<...>> turns a lambda into analysable data. EF Core translates Where(x => x.Age > 18) into SQL this way — pass a Func instead and it must pull the whole table and filter in memory.
常用场景
C#
var nums = new[] { 5, 2, 8, 1, 9 };
// 排序
Array.Sort(nums, (a, b) => b.CompareTo(a));
Console.WriteLine(string.Join(",", nums));
// 事件
var list = new List<int>();
Action<int> onAdd = n => Console.WriteLine("添加 " + n);
onAdd(1);
// 延迟执行
Func<int> lazy = () =>
{
Console.WriteLine("(计算中)");
return 42;
};
Console.WriteLine("还没算");
Console.WriteLine(lazy());Output
9,8,5,2,1
添加 1
还没算
(计算中)
42该不该用
- 该用 —— 一次性传逻辑(排序、过滤、事件处理器)
- 该用 —— LINQ 查询
- 不用 —— 逻辑超过 5 行,抽成命名方法更好读也好测试
- 不用 —— 需要被多处复用时(复制粘贴 lambda 会散落逻辑)
Exception stack traces from lambdas show names like
<Program>b__0_0 — hard to debug. Extract anything longer than a few lines into a named method; GetActiveUsers() beats x => x.Active && ... in a trace.两种写法
C#
// 匿名方法(C# 2.0)
Func<int, int> f1 = delegate (int x) { return x * 2; };
// lambda 语句体(C# 3.0)
Func<int, int> f2 = x => { return x * 2; };
// lambda 表达式体
Func<int, int> f3 = x => x * 2;
Console.WriteLine($"{f1(3)} {f2(3)} {f3(3)}");
// 无参数
Action a = () => Console.WriteLine("无参");
// 多参数
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(add(1, 2));Output
6 6 6
无参
3闭包捕获
C#
int counter = 0;
Action inc = () => counter++; // 捕获变量本身
inc();
inc();
inc();
Console.WriteLine(counter); // 3
// 捕获的是变量,不是当时的快照
int x = 10;
Action show = () => Console.WriteLine(x);
x = 20;
show(); // 20,不是 10Output
3
20「捕获变量而非值」是闭包最容易踩的坑。要在循环里为每个元素捕获独立的值,就在循环体内声明一个局部变量:
var local = i; 然后用 local。C# 5 起 foreach 的循环变量已自动每轮独立,但 for 的 i 仍是共享的。C#
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.Write(i + " ")); // 都捕获同一个 i
}
foreach (var a in actions) a();
Console.WriteLine();
var fixedActions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int local = i; // 每轮新变量
fixedActions.Add(() => Console.Write(local + " "));
}
foreach (var a in fixedActions) a();
Console.WriteLine();Output
3 3 3
0 1 2 表达式树
C#
using System.Linq.Expressions;
// 赋值给 Expression<T> 时,lambda 变成数据结构而不是代码
Expression<Func<int, bool>> expr = x => x > 5;
Console.WriteLine(expr.Body);
Console.WriteLine(expr.Parameters[0].Name);
// 可以分析它
if (expr.Body is BinaryExpression bin)
{
Console.WriteLine($"{bin.NodeType} {bin.Left} {bin.Right}");
}
// 也可以编译回委托
var func = expr.Compile();
Console.WriteLine(func(10));Output
(x > 5)
x
GreaterThan x 5
TrueExpression<Func<...>> 让 lambda 变成可分析的数据。EF Core 就是靠这个把 Where(x => x.Age > 18) 翻译成 SQL 的——如果传 Func 而不是 Expression<Func>,就只能全表拉到内存再过滤。
常用场景
C#
var nums = new[] { 5, 2, 8, 1, 9 };
// 排序
Array.Sort(nums, (a, b) => b.CompareTo(a));
Console.WriteLine(string.Join(",", nums));
// 事件
var list = new List<int>();
Action<int> onAdd = n => Console.WriteLine("添加 " + n);
onAdd(1);
// 延迟执行
Func<int> lazy = () =>
{
Console.WriteLine("(计算中)");
return 42;
};
Console.WriteLine("还没算");
Console.WriteLine(lazy());Output
9,8,5,2,1
添加 1
还没算
(计算中)
42该不该用
- 该用 —— 一次性传逻辑(排序、过滤、事件处理器)
- 该用 —— LINQ 查询
- 不用 —— 逻辑超过 5 行,抽成命名方法更好读也好测试
- 不用 —— 需要被多处复用时(复制粘贴 lambda 会散落逻辑)
lambda 里的异常堆栈只显示
<Program>b__0_0 这样的名字,排查困难。方法体超过几行就抽成命名方法——GetActiveUsers() 比 x => x.Active && ... 在堆栈里好认得多。