天天看点

C#学习笔记(三)—–C#高级特性:Lambda表达式

Lambda表达式

Lambda表达式是写在委托实例上的匿名方法。编译器立即将Lambda表达式转换成下面两种形式中的一种:

①委托实例

Expression<TDelegate>

类型的表达式树,该表达式树将Lambda表达式内的代码显示为可遍历的对象模型。这使得对Lambda的解释可以延迟到运行时。

下面的委托类型:

delegate int Trasform (int i);

可以指定和调用下面的Lambda表达式:

Trasform sqr = x => x * x; Console.WriteLine (sqr(3)); // 9

提示:编译器在内部将Lambda表达式编译成一个私有方法,并把表达式代码移到方法中:①首先Trasform委托被编译为下面的形式:

C#学习笔记(三)—–C#高级特性:Lambda表达式

②lambda方法被编译成一个私有的静态方法:

C#学习笔记(三)—–C#高级特性:Lambda表达式
  • lambda表达式有以下形式:(参数)=>表达式或语句块

    为了方便,在只有一个可推测类型的参数时,可以省略小括号。

  • lambda表达式使每一个参数的类型和委托的参数的类型一致,使表达式的返回类型和委托的返回类型一致。
  • lambda表达式代码可以是表达式也可以是语句块:在上例中,我们也可以这样写:
  • lambda表达式通常可Func或Action一起使用,上例也可以这样写:
Func<int,int> func=x =>{ x * x;};
           
  • 下面是一个带两个参数的lambda表达式:
Func<string,string,int> totalLength = (s1, s2) => s1.Length + s2.Length;
int total = totalLength ("hello", "world"); // total is 10;
           
  • Lambda表达式是C#3.0引入的概念。
  • 明确指定lambda表达式的参数类型:lambda表达式可以根据代码的上下文推断出参数类型,当不能推断出时,可以指定lambda的参数类型。考虑下面的代码:
Func<int,int> sqr = x => x * x;
           

编译器可以根据类型推断来判断x的类型是一个int。

我们可以上例中显示的为x指定一个类型:

Func<int,int> sqr = (int x) => x * x;
           
  • 捕获外部变量:Lambda表达式可以引用局部变量以及定义该lambda表达式的方法的参数:
static void Main()
{
int factor = ;
Func<int, int> multiplier = n => n * factor;
Console.WriteLine (multiplier ()); // 6
}
           
  • 上例中的factor对于lambda表达式来说,叫做外部变量(outer variable),lambda引用外部变量的过程叫做捕获外部变量。lambda捕获外部变量就叫做闭包(closure)。
  • 捕获的变量在真正调用委托时赋值,而不是在捕获的时候赋值:
int factor = ;
Func<int, int> multiplier = n => n * factor;
factor = ;
Console.WriteLine (multiplier ()); // 30
           
  • lambda表达式可以自动更新被捕获的变量:
int seed = ;
Func<int> natural = () => seed++;
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
Console.WriteLine (seed); // 2
           
  • 被捕获的变量的生命周期可以延长至捕获它的委托的生命周期(至少),在下面的例子中,局部变量seed本应在Natural()执行完后就消失了,但是由于seed被lambda表达式捕获了,所以它的生命周期被延长了:
static Func<int> Natural()
{
int seed = ;
return () => seed++; // Returns a closure
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
}
           

在lambda表达式内捕获已经实例化过的seed,在每次调用委托实例时都是唯一的,如果把上例中的seed的实例化过程放到lambda表达式内,则产生的结果不同:

static Func<int> Natural()
{
return() => { int seed = ; return seed++; };
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 0
}
           

提示:捕获局部变量的过程在内部的实施过程是这样的:创建一个私有类,并将被捕获的局部变量提升为这个私有类的字段(由局部变量级别提升至字段的级别)。这样,当方法被调用时,这个私有类被实例化,并将其生命周期绑定到委托的实例上。

  • 捕获循环变量:当捕获到的是一个for循环的循环变量,C#把这个循环变量当作是在for循环外部定义的。这意味着lambda在每次迭代中得到的循环变量的值都是一样的。
Action[] actions = new Action[];
for (int i = ; i < ; i++)
actions [i] = () => Console.Write (i);
foreach (Action a in actions) a(); // 333
           

上述生成的结果是333而不是012,原因是这样的:

①还是上面讲过的,捕获的变量是在调用委托时赋值,而不是在捕获的时候赋值,这意味着在调用action a in actions这句的时候捕获的变量才被赋值,而循环变量这个时候已经被迭代到了3。

②lambda表达式会自动更新值,这意味着lambda表达式总是会寻找到循环变量的最新值。循环变量的最终的最新的值就是3。

把for循环展开的话更容易理解:

Action[] actions = new Action[];
int i = ;
actions[] = () => Console.Write (i);
i = ;
actions[] = () => Console.Write (i);
i = ;
actions[] = () => Console.Write (i);
i = ;
foreach (Action a in actions) a(); // 
           

解决的方法是,把每次循环的变量都赋值非一个临时的变量,这个临时的变量放在lambda表达式的内部:

Action[] actions = new Action[];
for (int i = ; i < ; i++)
{
int loopScopedi = i;
actions [i] = () => Console.Write (loopScopedi);
}
foreach (Action a in actions) a(); // 012
           

这样,就可使闭包在每次循环迭代的时候捕获一个不一样的变量。

- 在c#5.0之前,foreach循环和for循环的工作原理是一样的。

Action[] actions = new Action[];
int i = ;
foreach (char c in "abc")
actions [i++] = () => Console.Write (c);
foreach (Action a in actions) a(); // ccc in C# 4.0
           

这会引起一些困惑,不同于for循环,foreach循环中的循环变量在每一次循环迭代中都是不可变得,人们期望的是可以将它当作是循环体中的局部变量,好消息是这个情况在C#5.0中被修改了,上例中在C#5.0中运行的结果是abc。

需要注意的是,这在技术上是一种毁灭性的改变:因为在c#5.0和C#4.0的运行结果完全不一样,之前在C#4.0及以前版本写的代码在C#5.0上面的运行结果完全不同!也可以看到,之所以做这样的修改,很明显这就是C#4.0的一个bug了。(要不然也不会做这样的修改)