閒話 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 中實驗成功

參考

相關文章
相關標籤/搜索