C# Lambda表达式
01 定义
Lambda表达式其实就是一个用来代替委托实例的未命名的方法
编译器会把Lambda表达式转化为以下二者之一:一个委托实例 , 一个表达式树(expression tree),类型是Expression,它表示了 可遍历的对象模型中Lambda表达式里面的代码。它允许lambda表达式延迟到运行时再被解释。
例子
delegate int Transformer(int i);
Transformer sqr = x => x * x;
Console.WriteLine(sqr(3)); //9
实际上,编译器会通过编写一个私有方法来解析这个Lambda表达式,然后把表达式的代码移动到这个方法里。
02 使用
//(参数) => 表达式或语句块
(parameters) => expression-or-statement-block
其中如果只有一个参数且类型可推断的话,那么参数的小括号可以省略
lambda表达式与委托
- 每个lambda表达式的参数对应委托的参数
- 表达式的类型对应委托的返回类型
语句块
lambda表达式的代码也可以是语句块
x => {return x*x;};
Func和Action 委托
Lambda表达式通常与Func和Action委托一起使用
Func<int,int> sqr = x => x*x;
Func<string,string,int> totalLength = (s1,s2) => s1.Length + s2.Length;
int tatal = totalLength("hello","world"); // total is 10
显示指定lambda表达式的参数类型
有时候参数类型推断不出来,这时候就要显示指定lambda表达式的参数类型
void Foo<T> (T x){}
void Bar<T> (Action<T> a){}
Bar(x=> Foo(x)); //x是什么类型?
Bar((int x) => Foo(x));
Bar<int>(x=> Foo(x)); //指定类型
Bar<int>(Foo(x)); //使用方法组
捕获外部变量
lambda表达式可以引用本地的变量和所在方法的参数
static void main(){
int f = 2;
Func<int,int> multi = x => x*f;
Console.WriteLine(multi(3)); //6
}
上面那个例子中f被叫做被捕获的变量,捕获了外部变量的lambda表达式叫做闭包(JavaScript也有个闭包的概念)
被捕获的变量,在委托被实际调用时才被计算,而不是定义时,上面的例子小改下
static void main(){
int f = 2;
Func<int,int> multi = x => x*f;
f = 10;
Console.WriteLine(multi(3)); //输出是 30
}
同时lambda表达式也可以更新被捕获的变量
int i = 0;
Func<int> add = ()=> i++;
Console.WriteLine(add()); //0
Console.WriteLine(add()); //1
Console.WriteLine(i); //2
被捕获的变量的生命周期会被延长到和委托一样
static Func<int> add(){
int i = 0; //知道下面的lambda表达式没有使用了,这个变量才销毁
return ()=> i++;
}
static void main(){
Func<int> f = add();
Console.WriteLine(f()); //0
Console.WriteLine(f()); //1
}
lambda表达式内的本地变量
在lambda表达式内实例化的本地变量对于委托实例的每次调用来说都是唯一的。
static Func<int> add(){
return () => {
int i = 0;
return i++;
};
}
static void main(){
Func<int> f = add();
Console.WriteLine(f()); //0
Console.WriteLine(f()); //0
}
捕获迭代变量
当捕获for循环的迭代变量时,C#会把这个变量当作是在循环外部定义的变量,这就意味着每次迭代捕获的都是同一个变量
Action[] actions = new Action[3];
for(int i=0;i<3;i++){
actions[i] = () => Console.WriteLine(i);
}
foreach(Action a in actions) a(); //3 3 3 理解上和捕获外部变量是一样的
那要如何解决上面的问题呢?小改下
Action[] actions = new Action[3];
for(int i=0;i<3;i++){
int k = i; //每个lambda表达式捕获的是不同的k,可以看k的作用域,和多线程很相似啊
actions[i] = () => Console.WriteLine(k);
}
foreach(Action a in actions) a(); //0 1 2
注意:foreach
C#4 和 C#5+的区别
Action[] actions = new Action[3];
int i=0;
foreach(char c in 'abc'){
actions[i++] = () => Console.WriteLine(c);
}
foreach(Action a in actions) a(); //C#4 输出是ccc 在C#5+ 输出是abc
lambda表达式 vs 本地方法
本地方法是C#7的一个新特性。
它和Lambda表达式在功能上有很多重 复之处,但它有三个优点:
• 可以简单明了的进行递归
• 无需指定委托类型(那一堆代码)
• 性能开销略低一点
本地方法效率更高是因为它避免了委托的间接调用(需要CPU周期, 内存分配)。本地方法也可以访问所在方法的本地变量,而且无需编 译器把被捕获的变量hoist到隐藏的类。
匿名方法 vs Lambda表达式
匿名方法和Lambda表达式很像,但是缺少以下三个特性:
- 隐式类型参数
- 表达式语法(只能是语句块)
- 编译表达式树的能力,通过赋值给Expressions
例子
delegate int Add(int i);
Add add = delegate (int x){return x*x}; //匿名方法
Console.WriteLine(add(3)); //9
Add add = x => x*x; //Lambda表达式
匿名方法因此也慢慢不用了