6

隐约记得 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");

上面的例子中,编译器会推导出 ddecimal 类型,所以可以调用带格式参数的 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 命名空间中定义的大量 ActionFunc 系列泛型委托中的一个。所以自己在使用委托的时候,也可以利用这些系统预定义的公共委托,省得自己再去定义。

题外话 ActionFunc 系列委托是 .NET 3.5 加入的。在这之前很多人需要无参数无返回值委托的时候,都偷懒使用 System.Threading.ThreadStart。现在可以换成 System.Action 了,虽然其本质是一样的,但是不利于人工理解。

2) 委托:事件

事件是委托的一种特殊应用。在没有 Lambda 之前,习惯使用私有命名方法来写事件。有了 Lambda 就可以大大简化了。

button.Click += (sender, args) => {
    MessageBox.Show("你点了我!");
}

不过由于 Visual Studio 在设置界面的时候会自动帮我们生成事件函数,所以,其实这种应用要相对少得很,毕竟自己写事件装载的人还是不多。

3) 匿名方法/局部方法/闭包

Lambda 表达式本来就是用来代替匿名方法的,所以可以用匿名方法的地方都可以用 Lambda。一个比较典型的情况就是(尤其是对 JavaScript 程序员来说),在某个方法中想临时定义一个方法来专项处理某些事情的时候,就可以使用 Lambda 表达式。当然,不能直接使用,需要使用 ActionFunc 包装。

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 中实验成功

参考


边城
59.8k 声望29.6k 粉丝

一路从后端走来,终于走在了前端!


引用和评论

0 条评论