闲话 Lambda

隐约记得 Lambda 表达式来源于 C# 5.0,但又不太肯定,因而查了下 百度百科:Lambda表达式,仍然没有获得明确的答案,因此懒得去纠结这个问题了。javascript

Lambda 概述

C# 有一种比较特殊的语言特性,叫“委托”。在 C#2.0 之前,只能使用命名方法来声明委托。C#2.0 引入了匿名方法。而在 C#3.0 中,引入 Lambda 表达式取代了匿名方法。因为 C#3.0 同时引入了 Linq 语法,因此能够认为 Lambda 表达式是为简化 Linq 写法而生的,虽然它并不只仅用于 Linq。java

Lambda 语法

C# 中 Lambda 表达式的运算符是 =>,这个个符号的左侧是参数,右侧是表达式或语句块。因此 Lambda 表达式的主要形式是这样node

(参数列表) => { 语句块 }

当“语句块”只有一条语句的时候,能够省略大括号,就成了程序员

(参数列表) => 语句

但这里“语句”不包括 “return 语句”。由于 return 后面必定是一个表达式。这种状况下,应该直接写表达式,省略掉 returnexpress

(参数列表) => 表达式

若是想加上 return,就要把大括号加上编程

(参数列表) => { return 表达式; }

通常状况下,若是 => 右边是语句,都会写成语句块的形式,因此 (参数列表) => 语句 这种形式是不多用的。那么经常使用的就是闭包

  • (参数列表) => 表达式
  • (参数列表) => { 语句块 }

以上各类形式统称 Lambda 表达式,但在 Resharper 中,上述经常使用的两种分别被称为 lambda expression 和 lambda statement。因此若是偶尔听到说 Lambda 语句,也不要吃惊。函数

Lambda 表达式的参数

因为 Lambda 表达式通常是做为参数或者值使用,因此根据使用的上下文,大部分状况下编译器能够推断出 Lambda 表达式的参数类型。正由于如此,Lambda 表达式的参数一般是省略类型的。好比this

Func<decimal, string> format = (d) => d.ToString("N2");

上面的例子中,编译器会推导出 ddecimal 类型,因此能够调用带格式参数的 ToString(若是没有推导出类型,而是把 d 看成 object,其 ToString 是不能带参数的,就会出现编译错误);另外,返回类型 string 也很容易根据 ToString() 的结果推导出来,若是把类型改成 Func<decimal, int> 就会出现编译错误。.net

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

参考

相关文章
相关标签/搜索