声明
本文内容来自微软 MVP solenovex 的视频教程——真会C#? - 第4章 委托、事件、Lambda表达式(完结),大致和第 4 课—— 4.3 4.4 Lambda表达式 对应。可在 GitHub 中查看 C# 视频教程的配套PPT
本文主要包括以下内容:
- 显式指定 Lambda 表达式的参数类型
- 捕获外部变量
- Lambda 表达式 vs 本地方法
- 匿名方法
Lambda 表达式
Lambda表达式其实就是一个用来代替委托实例的未命名的方法,编译器会把Lambda表达式转化为以下二者之一:
- 一个委托实例
- 一个表达式树(expression tree),类型是 Expression<TDelegate>,它表示了可遍历的对象模型中 Lambda 表达式里面的代码。它允许 Lambda 表达式延迟到运行时再被解释
delegate int Transformer (int i);
Transformer sqr = x => x * x;
Console.WriteLine (sqr(3)); // 9
实际上,编译器会通过编写一个私有方法来解析这个 Lambda 表达式,然后把表达式的代码移动到这个方法里。
Lambda表达式的形式,(参数)=> 表达式或语句块,(parameters) => expression-or-statement-block
其中如果只有一个参数并且类型可推断的话,那么参数的小括号可以省略
Lambda 表达式与委托,每个 Lambda 表达式的参数对应委托的参数,表达式的类型对应委托的返回类型。
x => x * x;
delegate int Transformer (int i);
Lambda 表达式的代码也可以是语句块。x => { return x * x; };
Lambda 表达式通常与 Func 和 Action 委托一起使用,
Func<int,int> sqr = x => x * x;
Func<string,string,int> totalLength = (s1, s2) => s1.Length + s2.Length;
int total = totalLength ("hello", "world"); // total is 10;
显式指定 Lambda 表达式的参数类型
void Foo<T> (T x) {}
void Bar<T> (Action<T> a) {}
Bar (x => Foo (x)); // What type is x?
Bar ((int x) => Foo (x));
Bar<int> (x => Foo (x)); // Specify type parameter for Bar
Bar<int> (Foo); // As above, but with method group
捕获外部变量
Lambda 表达式可以引用本地的变量和所在方法的参数。
static void Main()
{
int factor = 2;
Func<int, int> multiplier = n => n * factor;
Console.WriteLine (multiplier (3)); // 6
}
被捕获的变量
被 Lambda 表达式引用的外部变量叫做被捕获的变量(captured variables)。捕获了外部变量的 Lambda 表达式叫做闭包。被捕获的变量是在委托被实际调用的时候才被计算,而不是在捕获的时候。
int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30
Lambda 表达式本身也可以更新被捕获的变量。
int seed = 0;
Func<int> natural = () => seed++;
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
Console.WriteLine (seed); // 2
被捕获的变量的生命周期会被延长到和委托一样。
static Func<int> Natural()
{
int seed = 0;
return () => seed++; // Returns a closure
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
}
Lambda 表达式内的本地变量
在 Lambda 表达式内实例化的本地变量对于委托实例的每次调用来说都是唯一的。
static Func<int> Natural()
{
return() => { int seed = 0; return seed++; };
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 0
}
捕获迭代变量
当捕获 for 循环的迭代变量时,C# 会把这个变量当作是在循环外部定义的变量,这就意味着每次迭代捕获的都是同一个变量。
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
actions [i] = () => Console.Write (i);
foreach (Action a in actions) a(); // 333
Action[] actions = new Action[3];
int i = 0;
actions[0] = () => Console.Write (i);
i = 1;
actions[1] = () => Console.Write (i);
i = 2;
actions[2] = () => Console.Write (i);
i = 3;
foreach (Action a in actions) a(); // 333
如何解决每次迭代捕获的都是同一个变量
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
int loopScopedi = i;
actions [i] = () => Console.Write (loopScopedi);
}
foreach (Action a in actions) a(); // 012
注意:foreach,C#4,和 C#5+ 的区别。
Action[] actions = new Action[3];
int i = 0;
foreach (char c in "abc")
actions [i++] = () => Console.Write (c);
foreach (Action a in actions) a(); // ccc in C# 4.0
Lambda 表达式 vs 本地方法
本地方法是 C#7 的一个新特性。它和 Lambda 表达式在功能上有很多重复之处,但它有三个优点:
- 可以简单明了的进行递归
- 无需指定委托类型(那一堆代码)
- 性能开销略低一点
本地方法效率更高是因为它避免了委托的间接调用(需要 CPU 周期,内存分配)。本地方法也可以访问所在方法的本地变量,而且无需编译器把被捕获的变量 hoist 到隐藏的类。
匿名方法
匿名方法 vs Lambda 表达式
匿名方法和Lambda表达式很像,但是缺少以下三个特性:
- 隐式类型参数
- 表达式语法(只能是语句块)
- 编译表达式树的能力,通过赋值给 Expression<T>
delegate int Transformer (int i);
Transformer sqr = delegate (int x) {return x * x;};
Console.WriteLine (sqr(3));
Transformer sqr = (int x) => {return x * x;};
// Or simply:
Transformer sqr = x => x * x;
其它
捕获外部变量的规则和 Lambda 表达式是一样的。但匿名方法可以完全省略参数声明,尽管委托需要参数。public event EventHandler Clicked = delegate { };
这就避免了触发事件前的null检查。
// Notice that we omit the parameters:
Clicked += delegate { Console.WriteLine ("clicked"); };
参考
Lambda expressions (C# Programming Guide)
=> operator (C# reference)
Anonymous functions (C# Programming Guide)