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

来源:互联网 发布:唐筛标准年龄风险数据 编辑:程序博客网 时间:2024/05/20 20:19

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委托被编译为下面的形式:
委托的形式
②lambda方法被编译成一个私有的静态方法:
这里写图片描述

  • lambda表达式有以下形式:(参数)=>表达式或语句块
    为了方便,在只有一个可推测类型的参数时,可以省略小括号。
  • lambda表达式使每一个参数的类型和委托的参数的类型一致,使表达式的返回类型和委托的返回类型一致。
  • lambda表达式代码可以是表达式也可以是语句块:在上例中,我们也可以这样写:
Trasform sqr = x =>{ x * x;};
  • 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 = 2;Func<int, int> multiplier = n => n * factor;Console.WriteLine (multiplier (3)); // 6}
  • 上例中的factor对于lambda表达式来说,叫做外部变量(outer variable),lambda引用外部变量的过程叫做捕获外部变量。lambda捕获外部变量就叫做闭包(closure)。
  • 捕获的变量在真正调用委托时赋值,而不是在捕获的时候赋值:
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()); // 0Console.WriteLine (natural()); // 1Console.WriteLine (seed); // 2
  • 被捕获的变量的生命周期可以延长至捕获它的委托的生命周期(至少),在下面的例子中,局部变量seed本应在Natural()执行完后就消失了,但是由于seed被lambda表达式捕获了,所以它的生命周期被延长了:
static Func<int> Natural(){int seed = 0;return () => seed++; // Returns a closure}static void Main(){Func<int> natural = Natural();Console.WriteLine (natural()); // 0Console.WriteLine (natural()); // 1}

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

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

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

  • 捕获循环变量:当捕获到的是一个for循环的循环变量,C#把这个循环变量当作是在for循环外部定义的。这意味着lambda在每次迭代中得到的循环变量的值都是一样的。
Action[] actions = new Action[3];for (int i = 0; i < 3; 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[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

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

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

这样,就可使闭包在每次循环迭代的时候捕获一个不一样的变量。
- 在c#5.0之前,foreach循环和for循环的工作原理是一样的。

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

这会引起一些困惑,不同于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了。(要不然也不会做这样的修改)

阅读全文
0 0
原创粉丝点击