C# 匿名方法

`delegate` 匿名方法、lambda 表达式、闭包捕获、以及陷阱。

Intermediate
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,不是 10
Output
3
20
Capturing 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
True

Expression<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.

Friends & Partners