Lambda表达式
Lambada表达式是一种可以替代委托实例的匿名方法。编译器会立即将Lambda表达式转换为一下两种形式之一:
- 一个委托实例
- 一个类型为Expression
的表达式树(这个后面将)
匿名方法
上面说Lambada是一种匿名方法,那么就要先了解一下什么是匿名方法
匿名方法是C#2.0引入的特性
匿名方法的写法实在delegate关键字后面跟上参数的声明(可选),然后是方法体
using System;
class Program
{
delegate void Example();
static void Main(string[] args)
{
// Example e = delegate(){ Console.WriteLine("一个匿名方法的实现"); };
// 如果没有参数,可以省略参数的括号
Example e = delegate { Console.WriteLine("一个匿名方法的实现"); };
e();
}
}
这里声明了一个匿名方法,匿名方法解决的问题,是有时候想用委托,就必须需要一个方法,但是这个方法只是在这个委托中使用一下子,不需要在其他地方复用,于是引入了匿名方法
delegate(){ Console.WriteLine("一个匿名方法的实现"); };
匿名方法的写法其实与普通方法并无异样,只是用delegate关键字在前标注,省略掉方法名(如果不好理解,可以理解成方法名为delegate,没有参数可以省略括号,只能用于注册进委托)
匿名方法使用情况不多,因为C#3.0引入的Lambda更加强大,也是后面要讲的重点
匿名方法目前最广泛的用法,是用于声明空事件处理器的事件
public event EventHandler Clicked =delegate { };
Clicked事件不会进行任何操作,因为没有定义任何操作,但是Clicked不为空,不会抛异常,在用户层面,就是点击了某个按钮后没有任何变化,但是如果Clicked为空,就会抛异常
完全省略参数的声明是匿名方法独有的特性,即使委托需要这些参数声明,如上面声明空事件处理器的事件,EventHandler
其实需要一个object
的参数和一个EventArgs
类型的参数
Lambda表达式
Lambda是一种更强大匿名方法,前面讲了匿名方法,先来看看Lambda如何替代匿名方法
using System;
class Program
{
delegate void Example();
static void Main(string[] args)
{
// Example e = () => { Console.WriteLine("一个Lambda表达式"); };
// 当方法体只有一句时可以省略大括号
Example e = () => Console.WriteLine("一个Lambda表达式");
e();
}
}
从代码中可以看到,匿名方法被替换成了这样一句
() => { Console.WriteLine("一个Lambda表达式"); }
在Lambda表达式中=>
之前的是方法的参数,=>
之后是方法体
参数和方法体的编写规则
- 编译器通常可以根据上下文推断出Lambda表达式的类型,但是当无法推断的时候则必须显式指定每一个参数的类型
// 能够推断参数类型
(x) => { return x; }
// 不能推断参数类型
(int x) => { return x;}
- 没有参数,一个参数和多个参数时的写法
// 没有参数时小括号不能省略
() => { Console.WriteLine("一个Lambda表达式"); }
(x) => { return x; }
// 只有一个参数时可以省略小括号
x => { return x; }
// 多个参数时小括号不能省略
(x,y,z) => { return x+y+z; }
- 方法体只有一条语句的时候,可以省略大括号,return也可以省略;方法体有多条语句时大括号不能省略
x => { return x; }
// 一条语句可以省略大括号
x => return x;
// 一条语句可以省略return
x => x;
// 多条语句时不能省略大括号和return
x =>
{
Console.WriteLine("看看");
return x;
};
Lambda表达式的闭包和foreach
Lambda表达式可以引用方法内定义的局部变量和方法的参数(外部变量)
Lambda表达式所引用的外部变量称为捕获变量,捕获变量的表达式称为闭包
using System;
class Program
{
static void Main(string[] args)
{
int x = 2;
Func<int, int> sum = n => n + x;
Console.WriteLine(sum(10)); // 输出12
}
}
在这个例子中,x就是被捕获的变量
捕获变量的值
Lambda表达式捕获的变量是在调用委托时赋值,而不是在捕获时赋值
using System;
class Program
{
static void Main(string[] args)
{
int x = 2;
// 捕获外部变量x,但此时并没有赋值
Func<int, int> sum = n => n + x;
x = 10;
// 调用个委托时才赋值,此时x是10
Console.WriteLine(sum(10)); // 输出20
}
}
捕获变量的生命周期会延伸到和委托的生命周期一致
Lambda表达式foreach的两个版本
如果Lambda捕获迭代变量,最后会有怎样的结果
using System;
class Program
{
static void Main(string[] args)
{
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var a in actions)
{
a();
}
// 输出333
}
}
先来看这个例子,利用Lambda表达式捕获了for循环的i
变量,但此时i
的值并没有确定,前面说过,Lambda捕获的变量在调用时才赋值,所以这虽然捕获了三次i
,但这三次都是捕获的同一个i
,所以最后在调用时赋值了i
的最后的值3(前面说过,捕获变量的生命周期会延伸到和委托的生命周期一致,虽然for循环结束了,但是因为Lambda的捕获延长了生命周期,3这个值保留了下来),所以最后输出的是333
如果要解决这个问题,只需要将循环变量指定到内部的变量中,即
using System;
class Program
{
static void Main(string[] args)
{
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
int temp = i;
actions[i] = () => Console.WriteLine(temp);
}
foreach (var a in actions)
{
a();
}
// 输出012
}
}
对于lambda表达式来说,捕获了三次temp,但是每一次都是新定义的temp,所以不受影响
下面来看一个foreach的“BUG"
using System;
class Program
{
static void Main(string[] args)
{
Action[] actions = new Action[3];
int i = 0;
foreach (char c in "abc")
{
actions[i++] = () => Console.WriteLine(c);
}
foreach (Action action in actions)
{
action();
}
// 输出abc
}
}
这里输出结果是abc,这是因为foreach的每一个迭代变量都是不可变的,所以可以理解为循坏体中的局部变量,也就是类似于上面的temp
,但是,在C#5.0之前,结果并不是这样的,foreach会像前面的for语言一样解析,如果遇到老版本的代码,一定要特别注意