LINQ自學筆記-打地基-Lambda 運算式
因為參加 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。
--------
沒什麼特別的~
不過是一些筆記而已