您的位置:首页 > 编程语言 > C#

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

2017-05-22 11:54 260 查看

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()); // 0
Console.WriteLine (natural()); // 1
Console.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()); // 0
Console.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()); // 0
Console.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了。(要不然也不会做这样的修改)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: