Lambda表達式??

相信很多朋友一開始看到 => 跟我一樣的疑惑,我一開始聽到 Lambda 腦中浮現的想法是這個

What are Lambda Expressions

來自我童年偉大的遊戲 Half-Life

好回歸正題!!所以究竟什麼是𝜆

它是C#3.0出現的語法

至今已經有數十年的時間了,而且大量的廣泛運用在LINQ查詢語句中。

可能剛接觸的人會有一個誤解,Lambda表達式並不是LINQ特有的,它們只是C#語言的一部分,只是在使用LINQ查詢時會經常使用到,如果一開始就跟大家解釋Lambda表達式的定義,可能會很抽象並且難以理解,讓我們直接從code開始理解它吧。

Let's Get Ready To Rumble!!!

開啟一個新專案

還是像之前我提到的,學習一個知識環境越簡單越好,而且這次的Sample Code 也很簡單,讓我們開啟一個主控台的專案。


我們從LINQ中最容易入門的方法Any來講解Lambda表達式,它會去檢查當前集合中所有的element,只要有任一個符合條件就會回傳布林值True,如果都不符合則會回傳 False

先創造出一組Array,我這邊是塞入費氏數列,有奇數跟偶數比較方便演示,數字可以隨便定義

我們來寫一個方法來檢查裡面這些整數是否有大於200

static bool IsAnyLargerThan200(int[] numberArray)
{
    foreach (var number in numberArray)
    {
        if (number > 200)
        {
            return true;
        }
    }
    return false;
}

這個方法相當的簡單,它只是會去iterates 整個集合的數字,如果其中有任一個數字大於200就會回傳True,沒有的話就會返回false,但他也很具體,他只能處理集合中是否有大於200的數字。

那假如說今天需求改變了,我們要檢查是否集合中是否有偶數呢?

static bool IsAnyEvenNumber(int[] numberArray)
{
    foreach (var number in numberArray)
    {
        if (number % 2 == 0)
        {
            return true;
        }
    }
    return false;
}
這個方法與我們上面寫的檢查是否有數字超過200的方法,重複度非常的高。

假如今天需求又改變了,我需要檢查集合中是否有質數,或是集合中是否有小於0的負數。

我們會需要創建越來越多幾乎一模一樣的方法,而且他們只會在if 判斷式這個部分有所不同而已。

這樣其實不是一個好現象,而且已經可以聞到了程式碼的壞味道(Bad Smell)

如果我們可有一個方法直接在if的判斷式這邊,然後他只回傳布林值那就很理想

讓我們來對我們的程式碼改造一下

static bool IsAnyLargerThan200(int[] numberArray)
{
    foreach (var number in numberArray)
    {
        if (IsLargerThan200(number))
        {
            return true;
        }
    }
    return false;
}

static bool IsLargerThan200(int number)
{
    return number > 200;
}

static bool IsAnyEvenNumber(int[] numberArray)
{
    foreach (var number in numberArray)
    {
        if (IsEven(number))
        {
            return true;
        }
    }
    return false;
}

static bool IsEven(int number)
{
    return number % 2 == 0;
}
 

經過我們的改良過後,我們目前兩個方法的差異性只在這邊了

如果我們能將IsLargerThan200、IsEven 這兩個方法合而為一

並且當作參數傳遞進來,似乎是更好的做法。


委派(Delegate)

在C#當中我們能否將method如同常用的string,int,bool 或其他的類型一樣當作參數一樣傳遞?

是的我們可以利用委派(Delegate),如果有再寫C/C++的朋友,委派類似函數指針的概念

使用委派我們可以將method作為參數傳遞,並且還能將它們賦值給 variables


static bool IsAny(int[] numberArray, Func<int, bool> predicate)
{
    foreach (var number in numberArray)
    {
        if (predicate(number))
        {
            return true;
        }
    }
    return false;
}

這裡我使用了C#中的泛型委派Func<>,在C#中有兩種泛型委派 Func<> 以及 Action<>

Func<> 是有回傳值的泛型委派 ,Action<>是沒有回傳值的泛型委派

這裡的第一個泛型參數是 該 method 要傳入的參數類型

第二個泛型參數是返回值的類型

我們可以使用Func<>來定義具有多個參數的method

類似這樣,我們只要記得最後一個參數都是返回的類型

在我們目前的例子中 predicate 這個參數是接收int 作為輸入;並返回 bool 的 method

剛好我們現在的方法 IsLargerThan200 以及 IsEven 都是這樣的method

都是接收int 作為輸入;並返回 bool


var numberArray = new int[] { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987 };


Console.WriteLine($"集合是否中是否有大於200的數字: {IsAny(numberArray, IsLargerThan200)}");
Console.WriteLine($"集合是否中是否有偶數: {IsAny(numberArray, IsEven)}");


static bool IsAny(int[] numberArray, Func<int, bool> predicate)
{
    foreach (var number in numberArray)
    {
        if (predicate(number))
        {
            return true;
        }
    }
    return false;
}

static bool IsLargerThan200(int number)
{
    return number > 200;
}

static bool IsEven(int number)
{
    return number % 2 == 0;
}
這是目前我們現在的code

看起來不錯喔!我們的code現在是可以正常編譯的,我們在這邊已經將method當作一個參數傳入

我們來看一下是不是能正常運行

看起來不錯喔,我們的Array確實有大於200的數字、在裡面也有偶數。


What are Lambda Expressions

好的目前我們知道,在C#中我們可以將函數作為參數傳遞

那麼Lambda表達式是用來做什麼的呢?

Lambda表達式 用來建立匿名方法,這表示方法沒有具體名稱

我們現在有兩個具體的方法 IsLargerThan200、IsEven

但假如我們又有了新的需求是要檢查 是否有奇數、是否有質數、是否有小於0的數字

又要寫很多很具體的方法,有沒有可以解決這樣需求的方式呢??

沒錯就是Lambda表達式

現在讓我們把IsLargerThan200、IsEven 這兩個方法都刪掉

var numberArray = new int[] { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987 };


Console.WriteLine($"集合是否中是否有大於200的數字: {IsAny(numberArray, number => number > 200)}");
Console.WriteLine($"集合是否中是否有偶數: {IsAny(numberArray, number => number % 2 == 0)}");


static bool IsAny(int[] numberArray, Func<int, bool> predicate)
{
    foreach (var number in numberArray)
    {
        if (predicate(number))
        {
            return true;
        }
    }
    return false;
}
現在完整的程式碼

讓我們來看一下發生了什麼

第二個參數我傳遞了Lambda表達式,它宣告了一個method,但是這個method是沒有名稱的,並且用一個特殊的語法宣告。

在這邊的這個箭頭=>叫做 Lambda Declaration Operator

箭頭左邊的內容是method parameters 也就是在目前的情況下我們有一個參數稱為 number。

如果我們想要一個Lambda表達式帶入多個參數的話我們可以這樣宣告

就像這樣,我們宣告了兩個參數 number 跟 point

在箭頭符號=>的右邊則是method的主體。

OK 我第一次看到這樣的語法的時候也很困惑

像是我們是在那裡定義參數的類型的?

我們並沒有定義我們的參數類型,參數的類型是compiler依據我們的context(上下文),自動推斷出來的。

它會知道你目前的參數是int型別

這又是為什麼呢?

我們在這邊定義的泛型委派,是定義輸入的參數要是int,因此compiler知道Lambda表達式必須是以int為參數的method。

就是這樣推斷出Lambda表達式需要帶入的參數型別,以及返回類型!

那又有一個疑問method 回傳的關鍵詞 return又在哪呢?

它只是將箭頭=>的右側部分當作method的結果然後return

有點類似這邊有個return的關鍵字。

這可能聽起來很抽象,而且不是那麼容易理解

再看看我們傳統的method,這樣聯想起來就更具體一些了

static bool IsLargerThan200(int number)
{
    return number > 200;
}

如果我們的Lambda表達式 比較複雜,我們也可以使用一個{},然後在裡面定義我們的method主體,這樣看起來會更像傳統的方法。

對比一下其實這兩段code的意思是一樣的,只是上面使用{}在定義method主體的時候,我們還需要加入return關鍵字,如同C#普通的method一樣。

我們能不能將Lambda表達式 用一個variable來引用

是可以的,我們可以將Lambda表達式 賦值給Func<>類型的變數

那我們能否用var 隱含式宣告?

var 不能用於宣告匿名method。

正如我們所說的Lambda表達式是依造context推斷出來的

compiler 需要知道這個參數必須式int,使用var 隱含式宣告時compiler沒辦法判斷 number這個參數的類型。

我們通常會使用Lambda表達式來宣告不會重複使用,而且較為簡單的method

LINQ方法都使用Func<>當作參數的傳遞

這也是為什麼當我們在使用LINQ方法時會大量使用Lambda表達式的原因了。

再回來看看我們現在的code


var numberArray = new int[] { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987 };

Console.WriteLine($"集合是否中是否有大於200的數字: {IsAny(numberArray, number => number > 200)}");
Console.WriteLine($"集合是否中是否有偶數: {IsAny(numberArray, number => number % 2 == 0)}");


static bool IsAny(int[] numberArray, Func<int, bool> predicate)
{
    foreach (var number in numberArray)
    {
        if (predicate(number))
        {
            return true;
        }
    }
    return false;
}

我們的IsAny 方法現在只能接受int型別的集合,如果我們想將它改成LINQ中的Any方法,能將任意型別當作參數傳入,我們又該怎麼做呢?

static bool IsAny<T>(IEnumerable<T> collection, Func<T, bool> predicate)
{
    foreach (var number in collection)
    {
        if (predicate(number))
        {
            return true;
        }
    }
    return false;
}

我們可以用一個更抽象的類型IEnumerable<T>來讓它接收任意型別的集合

這裡的T代表泛型型別參數Generic Type Parameter

讓我們來驗證這一點

var animalList= new List<string> { "Cat", "Bird", "Fish", "Dog", "Elephant", "Lion", "Tiger", "Monkey", "Panda", "Rabbit" };

我們現在來檢查這個List裡面有沒有包含Dog的字串

完全沒問題,可以編譯也可以執行,也是我們預期的結果。

到這邊基本上我們已經自己實現了LINQ中的Any方法!

希望看到這邊的朋友能夠對Lambda有一個基礎的了解,它的確需要掌握一些C#的基礎知識才能夠理解,需要先了解委派、泛型、多型 等知識。

我覺得如果能用影片來操作將可以更好的表達這些觀點,有機會我會再整理一下錄製一部影片用
LINQPad來 Demo,也能補充更多不方便用圖文講解的地方。

Visited 106 times, 1 visit(s) today

Leave A Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *