iT邦幫忙

DAY 10
5

分享一些學習心得系列 第 10

LINQ自學筆記-打地基-Lambda 運算式

Lambda 運算式是 .Net 3.5 新增加,我覺得很棒的一個特色,它並不是全新的功能,只是匿名委派的再精簡語法,但是實用性大增,請見內文分享。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
Lambda 運算式就是匿名委派的簡化版本,不過相對的,因為精簡到極致,所以若一開始就接觸它,會有理解上的困難,但是若從「具名委派→匿名委派」都了解,那 Lambda 運算式就不是什麼大問題了。

MSDN 對於 Lambda 運算式的定義如下:
「Lambda 運算式」(Lambda Expression) 是一種匿名函式,它可以包含運算式和陳述式 (Statement),而且可以用來建立委派 (Delegate) 或運算式樹狀架構型別。
所有的 Lambda 運算式都會使用 Lambda 運算子 =>,意思為「移至」。 Lambda 運算子的左邊會指定輸入參數 (如果存在),右邊則包含運算式或陳述式區塊。 Lambda 運算式 x => x * x 的意思是「x 移至 x 乘以 x」。

最簡單的理解方式,就是把匿名委派和 Lambda 運算式寫在一起看,就會清楚多了:

//宣告端 
public class 豪宅 { 
    public void 蟲出沒(Func<string, string> 人){ 
        Console.WriteLine(人("蟑螂")); 
    } 
    public void 整理書房(Func<Master, Location, string> 人){ 
        Master m = new Master(); 
        m.Name = "安琪"; 
        Location l = new Location(); 
        l.Name = "三樓"; 
        Console.WriteLine(人(m, l)); 
    } 
} 
//呼叫端 - 管家派工 
void Main()  { 
  豪宅 白宮 = new 豪宅(); 
  //匿名委派的寫法 
  白宮.蟲出沒(delegate(string 蟲) { 
                return 蟲 + " 死光光。";} 
              ); 
  //Lambda 運算式的寫法 1 
  白宮.蟲出沒(蟲 => 蟲 + " 死光光。"); 
  
  //匿名委派的寫法 
  白宮.整理書房(delegate(Master 主人, Location 地點) { 
                        return 主人.Name + " 的 " + 地點.Name + "書房整理好了。";} 
                    ); 
  //Lambda 運算式的寫法 2 
  白宮.整理書房((主人, 地點) => { 
                        return 主人.Name + " 的 " + 地點.Name + "書房整理好了。";} 
                    ); 
}

public class Master{ 
    private string name; 
    public string Name {get {return name;} set {name = value;}} 
} 
public class Location { 
    private string name; 
    public string Name {get {return name;} set {name = value;}} 
}

上述範例中,匿名委派寫法一致,但是 Lambda 運算式寫法有兩種,就是 MSDN 定義中所言,運算式和陳述式:

  1. 運算式 Lambda(Expression Lambda):委派的邏輯只需要一行就可以寫完,可採用此方式 (input parameters) => expression。這種寫法的特別就是,不需要寫 return 關鍵字,也不需要用大括號把邏輯包起來。常見的有下面四種寫法:

    (int x, string s) => s.Length > x; //明確指定傳入參數的型別,適用在無法型別推斷的時候。
    (a, b) => a + b; //讓編譯器使用型別推斷省去撰寫傳入參數型別的寫法。
    a => a * a; //只有一個傳入參數時,可以省略圓括號。
    () => "L" + "I" + "N" + "Q"; //沒有傳入參數時,必須用空的圓括號。

  2. 陳述式 Lambda(Statement Lambda):委派的邏輯必須用兩行以上程式碼才能完成,就必須選用此方式 (input parameters) => {statement;}。這種寫法和匿名委派相比較,其實就是把 delegate 關鍵字省略成 「=>」運算子而已。所以了解匿名委派的寫法,那使用陳述式 Lambda 應當是毫無問題,常見寫法和運算式寫法雷同,其實也就是加上大括號和 return 關鍵字而已:

    (int x, string s) => {x = x * 2; return s.Length > x;}
    (a, b) => {a = a + b; return a * b;}

在 LINQ 中,大多方法都提供 Func 的傳入參數,也就是都可以透過匿名委派傳入自定義的邏輯,例如:從一個數列中取奇數:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 }; 
int oddNumbers = numbers.Count(n => n % 2 == 1); 

因此了解並且知道如何撰寫 Lambda 運算式,絕對有助於應用 LINQ。

Lambda 運算式和匿名委派,除了語法的更精簡之外,實務上我覺得還有一個特色非常棒:Lambda 運算式的型別推斷很強悍,大多數情況下,都可以省略傳入參數的型別,以本文一開始的例子:

void Main()  { 
  豪宅 白宮 = new 豪宅(); 
  //匿名委派的寫法 
  白宮.蟲出沒(delegate(string 蟲) { 
                return 蟲 + " 死光光。";} 
              ); 
  //Lambda 運算式的寫法 1 
  白宮.蟲出沒(蟲 => 蟲 + " 死光光。"); 
  
  //匿名委派的寫法 
  白宮.整理書房(delegate(Master 主人, Location 地點) { 
                        return 主人.Name + " 的 " + 地點.Name + "書房整理好了。";} 
                    ); 
  //Lambda 運算式的寫法 2 
  白宮.整理書房((主人, 地點) => { 
                        return 主人.Name + " 的 " + 地點.Name + "書房整理好了。";} 
                    ); 
}

我們看到上述兩個匿名委派,都必須指名傳入參數的型別,但是 Lambda 運算式都可以省略(留給編譯器去推斷),因此就算使用陳述式 Lambda,都還是比匿名委派更方便。

最後再分享 Lambda 運算式/匿名委派 的一個特色:他們可以存取邏輯定義範圍內的外部變數,就算該變數已被回收,一樣可以使用,因為定義邏輯時,他會把該變數快取起來。請注意,下述範例的寫法和之前委派範例不大一樣:委派的邏輯被綁到宣告端處理,呼叫端只有調用方法和引用另一個委派而已。在 LINQ 一般應用中,幾乎不會有這樣的寫法,不過若是在非同步處理的程式中,有時就會看到這樣的寫法:

//Variable Scope in Lambda Expressions 
public class TestVarScope 
{ 
    public Func<bool> CompareMethodParaGtStand; 
    public Func<int, bool> CompareDelegateParaEqInput; 
    public void TestMethod(int inputNum) 
    { 
        int StandNum = 10; 
        Console.WriteLine ("初始標準值 = " + StandNum); 
        //定義委派邏輯,會把標準值給換掉 
        CompareMethodParaGtStand = () => {StandNum = 99; return inputNum > StandNum;}; 
        Console.WriteLine ("定義委派後,尚未叫用前,標準值 = " + StandNum); 
        bool retBool = CompareMethodParaGtStand.Invoke(); 
        Console.WriteLine ("叫用會改變標準值的委派後,標準值 = " + StandNum); 
        Console.WriteLine ("比對方法傳入參數是否大於標準值 = " + retBool); 
        Console.WriteLine ("目前方法傳入參數值 = " + inputNum + 
                                ", 目前的標準值 = " + StandNum); 
        //定義委派邏輯 
        CompareDelegateParaEqInput = num => num == StandNum; 
        //CompareDelegateParaEqInput = delegate(int num) {return num == StandNum;};   ←這行只是用來表達匿名委派的寫法。
    } 
}

void Main() 
{ 
    var obj = new TestVarScope(); 
    obj.TestMethod(50); 
    var num = obj.CompareDelegateParaEqInput(99); 
    Console.WriteLine ("比對委派傳入參數是否等於標準值 = " + num); 
}
/* 輸出:
初始標準值 = 10
定義委派後,尚未叫用前,標準值 = 10
叫用會改變標準值的委派後,標準值 = 99
比對方法傳入參數是否大於標準值 = False
目前方法傳入參數值 = 50, 目前的標準值 = 99
比對委派傳入參數是否等於標準值 = True
*/

宣告端:定義了兩個公開的委派,第一個委派(CompareMethodParaGtStand)會在 TestVarScope.TestMethod 方法中定義邏輯並引動,第二個委派(CompareDelegateParaEqInput)則會在 TestVarScope.TestMethod 方法中定義邏輯,但是由呼叫端引動。

呼叫端:建立 TestVarScope 的執行個體,然後調用 TestMethod 方法,引動第一個委派(CompareMethodParaGtStand)執行設定的邏輯(將標準值修改為 99,然後把方法參數和標準值比大小),並設定第二個委派(CompareDelegateParaEqInput)的邏輯,然後再引動第二個委派,並傳入和 CompareMethodParaGtStand 所修改後的標準值相同之數字,看是否相等。

這個範例要表達兩個重點:

  1. 定義委派的邏輯時,可以引用邏輯定義範圍內的變數(此範例中,範圍就是 TestMethod 方法),前提是該變數必須有被指派值。例如本範例中,若一開始沒有設定 StandNum = 10,只有宣告有這個變數,則在定義 CompareMethodParaGtStand 邏輯時,就會出現「使用未指定的區域變數」之編譯錯誤。
  2. 定義委派邏輯時所使用的外部區域變數,會被快取下來,供委派引動時使用。以本範例來說,就是 StandNum 這個區域變數,它是定義在 TestMethod 方法中,而且在呼叫端調用 TestMehtod 後,應該就要消失,但是因為在 CompareDelegateParaEqInput 中有設定要拿它來和委派的傳入參數做比較,所以當 CompareDelegateParaEqInput 引動時,它的值 99 仍然存在,因此比較結果是 True。

注意喔,因為 CompareDelegateParaEqInput 邏輯是在 TestMethod 方法中定義,所以一定要先叫用 TestMethod 方法,才能引動 CompareDelegateParaEqInput 委派,若反過來執行,CompareDelegateParaEqInput 會出現 NullReferenceException。


上一篇
LINQ自學筆記-打地基-Func委派、Action委派
下一篇
LINQ自學筆記-打地基-物件和集合初始設定式
系列文
分享一些學習心得30

尚未有邦友留言

立即登入留言