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

  • 4282
  • 0
  • 2013-01-10

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

 

Dotblogs 的標籤: , ,

因為參加 ITHome 的鐵人賽,所以整理了自己學習 LINQ 時的資料,變成自學筆記系列,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。

另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。

PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad

-------------------本文開始--------------

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。

--------
沒什麼特別的~
不過是一些筆記而已