隐约记得 Lambda 表达式来源于 C# 5.0,但又不太确定,于是查了下 百度百科:Lambda表达式,仍然没有得到明确的答案,所以懒得去纠结这个问题了。
Lambda 概述
C# 有一种比较特殊的语言特性,叫“委托”。在 C#2.0 以前,只能使用命名方法来声明委托。C#2.0 引入了匿名方法。而在 C#3.0 中,引入 Lambda 表达式取代了匿名方法。由于 C#3.0 同时引入了 Linq 语法,所以可以认为 Lambda 表达式是为简化 Linq 写法而生的,虽然它并不仅仅用于 Linq。
Lambda 语法
C# 中 Lambda 表达式的运算符是 =>
,这个个符号的左侧是参数,右侧是表达式或语句块。所以 Lambda 表达式的主要形式是这样
(参数列表) => { 语句块 }
当“语句块”只有一条语句的时候,可以省略大括号,就成了
(参数列表) => 语句
但这里“语句”不包括 “return 语句”。因为 return
后面一定是一个表达式。这种情况下,应该直接写表达式,省略掉 return
(参数列表) => 表达式
如果想加上
return
,就要把大括号加上(参数列表) => { return 表达式; }
一般情况下,如果 =>
右边是语句,都会写成语句块的形式,所以 (参数列表) => 语句
这种形式是很少用的。那么常用的就是
(参数列表) => 表达式
(参数列表) => { 语句块 }
以上各种形式统称 Lambda 表达式,但在 Resharper 中,上述常用的两种分别被称为 lambda expression 和 lambda statement。所以如果偶尔听到说 Lambda 语句,也不要吃惊。
Lambda 表达式的参数
由于 Lambda 表达式一般是作为参数或者值使用,所以根据使用的上下文,大部分情况下编译器可以推断出 Lambda 表达式的参数类型。正因为如此,Lambda 表达式的参数通常是省略类型的。比如
Func<decimal, string> format = (d) => d.ToString("N2");
上面的例子中,编译器会推导出 d
是 decimal
类型,所以可以调用带格式参数的 ToString
(如果没有推导出类型,而是把 d
当作 object
,其 ToString
是不能带参数的,就会出现编译错误);另外,返回类型 string
也很容易根据 ToString()
的结果推导出来,如果把类型改为 Func<decimal, int>
就会出现编译错误。
Lambda 表达式的目的之一就是简洁,所以,当只有一个参数的时候,可以省略参数列表两端的括号,那么上面的示例可以写成
Func<decimal, string> format = d => d.ToString("N2");
另外,在某些时候,编译器不能推导出参数类型,那就需要为参数指定类型,在这种情况下,虽然只有一个参数,括号也不能省略了。比如上面的例子可以改成
Func<decimal, string> format = (decimal d) => d.ToString("N2");
最后还有一种情况,编译器不能推导出返回值类型,那就需要进行类型转换处理,比如
Func<string> test = () => ((object) 123) as string;
这个例子除了说明语法之外,毫无意义。实际应用中有可能需要将某个
object
类型的引用转换成指定类型返回,就应该使用类似的语法。
什么时候使用 Lambda 表达式
1) 以 Linq 为代表的委托
Lambda 表达式最常用于委托。Linq 中大量使用委托,所以在写 Linq 的时候,会大量使用 Lambda。比如从 Person 列表中找出年龄小于20的
class Person {
public string Name { get;set; }
public int Age { get; set; }
}
Person[] FindYounger(IEnumerable<Person> persons) {
// linq 写法
// return (from p in persons
// where p.Age < 20
// select p).ToArray();
// 方法链写法
return persons.Where(p => p.Age < 20).ToArray();
}
linq 写法更像是 SQL,不过方法链写法的 Where()
方法调用很明显的使用了 Lambda 表达式作为参数。
来看看 Where()
扩展方法的定义
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate
)
除 this
参数之外,用于判断去留的参数是 Func<TSource, bool>
,这是在 System
命名空间中定义的大量 Action
和 Func
系列泛型委托中的一个。所以自己在使用委托的时候,也可以利用这些系统预定义的公共委托,省得自己再去定义。
题外话
Action
和Func
系列委托是 .NET 3.5 加入的。在这之前很多人需要无参数无返回值委托的时候,都偷懒使用System.Threading.ThreadStart
。现在可以换成System.Action
了,虽然其本质是一样的,但是不利于人工理解。
2) 委托:事件
事件是委托的一种特殊应用。在没有 Lambda 之前,习惯使用私有命名方法来写事件。有了 Lambda 就可以大大简化了。
button.Click += (sender, args) => {
MessageBox.Show("你点了我!");
}
不过由于 Visual Studio 在设置界面的时候会自动帮我们生成事件函数,所以,其实这种应用要相对少得很,毕竟自己写事件装载的人还是不多。
3) 匿名方法/局部方法/闭包
Lambda 表达式本来就是用来代替匿名方法的,所以可以用匿名方法的地方都可以用 Lambda。一个比较典型的情况就是(尤其是对 JavaScript 程序员来说),在某个方法中想临时定义一个方法来专项处理某些事情的时候,就可以使用 Lambda 表达式。当然,不能直接使用,需要使用 Action
或 Func
包装。
string GetFormatedAmount() {
decimal price = GetPrice();
decimal number = GetNumber();
return new Func<decimal>(() => {
return decimal.Round(price * number, 2);
}).Invoke().ToString("N2");
}
这个例子同样只是为了示例,实际意义不大。但是从这个例子中可以发现 Lambda 的一个特点——闭包。请注意到,这里 Lambda 表达式中用于计算的变量都是局部变量,而非通过参数传入。换个更明显的例子
Func<decimal> GetAlgorithm(int type) {
decimal price = GetPrice();
decimal number = GetNumber();
switch (type) {
case 1:
// 8折
return () => {
return decimal.Round(price * number * 0.8m, 2);
};
case 2:
// 满10赠1
return () => {
var payNumber = number > 10m ? number - 1 : number;
return decimal.Round(price * payNumber, 2);
};
default:
// 无优惠
return () => {
return decimal.Round(price * number, 2);
};
}
}
void Calc() {
var algorithm = GetAlgorithm(GetType());
var amount = algorithm();
Console.WriteLine(amount);
}
其它语言中的 Lambda
Java8 的 Lambda
Java8 中增加了 Lambda 表达式语法,与 C# 不同,运算符是用的 ->
而不是 =>
,也许这会让 C++ 转 Java 的程序员抓狂,不过对于纯粹的 Java 程序员来说,就是一个符号而已。
不过刚才说了,Lambda 表达式的作用其实就是匿名方法,而 Java 中并没有匿名方法这一语法。不过 Java 中有匿名对象,当你直接 new
一个接口并实现接口方法的时候,Java 编译器实际是产生了一个类(匿名类)来实现这个接口,然后再返回这个类的一个实例,也就是匿名对象。
ActionListner listener = new ActionListener() {
public void actionPerformed(ActionEvenet e) {
System.out.println("Hello, anonymous object");
}
};
就上面这个例子,如果提取其关键语法去匹配 Lambda 的语法定义,很容易就变成了
// 仅抽象语法,不能编译通过
(e) -> {
System.out.println("Hello, anonymous object");
}
问题在于,Java 的 lambda 一定是某个接口的实例,所以它必须有目标类型。上例中单纯的 Lambda 是 Java 编译器不能编译的,因为没有目标类型,不知道该生成一个什么类型的对象。所以正确的写法应该是
ActionListener listener = e -> {
System.out.println("hello anonymouse object");
};
如果目标变量类型不明确的时候,需要申明其类型
Object listener = (ActionListener) e -> {
System.out.println("hello anonymouse object");
};
关于 Java 的 Lambda,这里提到的只是皮毛,推荐大家看看这篇博客:Java8 Lambda 表达式教程
JavaScript 的 Lambda 表达式——箭头操作符
ES6 为 JavaScript 加入了 Lambda 表达式的新语法。不过在 JavaScript 中不叫 Lambda,叫箭头操作符,使用的和 C# 一样是 =>
。
基于对于 JavaScript 来说,Lambda 作用并不大,因为 JavaScript 的函数就是对象,定义自由,使用也很自由。不过箭头操作符仍然带来了大家喜欢它的理由……解决了 this
指针混淆的问题。还是来看例子
var person = {
name: "James",
friends: ["Jack", "Lenda", "Alpha" ],
shakeAll: function() {
this.friends.forEach(function(friend) {
console.log(`${this.name} shake with ${friend}`);
});
}
}
person.shakeAll();
这个没有 箭头操作符的例子,看起来没有什么不对,但是运行出来却跟预期的不一样,因为在 forEach
中的 function
用错了 this
。所以 shakeAll
应该改成
shakeAll: function() {
var _this = this;
this.friends.forEach(function(friend) {
console.log(`${_this.name} shake with ${friend}`);
});
}
但是如果用箭头操作符,就不用担心这个问题了
shakeAll: function() {
this.friends.forEach(friend => {
console.log(`${this.name} shake with ${friend}`);
});
}
然而这个特性在很多 JavaScript 引擎中都还没实现,至少 Chromium 42.0.2311 没实现,io.js 3.2 没实现。不过在 Firefox 40 中试验成功。
补充:在 node.js 4.0 中实验成功
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。