在C#2.0中引入了匿名方法,允許在期望出現委託的時候以“內聯”的代碼替代之。儘管匿名方法提供了函數式程式設計語言中的很多表達能力,但匿名方法的文法實在是太羅嗦了,並且很不自然。Lambda運算式為書寫匿名方法提供了一種更加簡單、更加函數化的文法。
實際上Lambda運算式的本質是匿名方法,也即是當編譯我們的程式碼時,編譯器會自動幫我們將Lambda運算式轉換為匿名方法。
20.5.1 建立Lambda運算式
Lambda運算式的書寫方式是一個參數列表後跟“=>”記號,然後跟一個運算式或一個語句塊,即Lambda運算式的文法格式為:
參數列 => 語句或語句塊
Lambda運算式例子如下所示:
delegate int del(int i);
...
del myDelegate = x => x * x;
int j = myDelegate(5); //j = 25
關於“參數列”,Lambda運算式的參數列可以具有顯式的或隱式的類型。在一個具有顯式類型的參數列表中,每個參數的類型都是顯式聲明的。在一個具有隱式類型的參數列表中,參數的類型是從Lambda運算式出現的上下文中推斷出來的——具體來說,是當Lambda運算式被轉換為一個相容的委託類型時,該委託類型提供了參數的類型。
當Lambda運算式只有一個具有隱式類型的參數時,參數列表中的括弧可以省略。即:
(param) => expr
可以簡寫為:
param => expr
最後,參數列中可包含任意個參數(與委託對應),如果參數列中有0個或1個以上參數,則必須使用括弧括住參數列,如下:
() => Console.Write("0個參數");
i => Console.Write("1個參數時參數列中可省略括弧,值為:{0}", i);
(x, y) => Console.Write("包含2個參數,值為:{0}:{1}", x, y);
而“語句或語句塊”中如果只有一條語句,則可以不用大括弧括住,否則則必須使用大括弧,如下所示:
i => Console.Write("只有一條語句");
i => { Console.Write("使用大括弧的運算式"); };
//兩條語句時必須要大括弧
i => { i++; Console.Write("兩條語句的情況"); };
如果“語句或語句塊”有傳回值時,如果只有一條語句則可以不寫“return”語句,編譯器會自動處理,否則必須加上,如下樣本:
class Test
{
delegate int AddHandler(int x, int y);
static void Print(AddHandler add)
{
Console.Write(add(1, 3));
}
static void Main()
{
Print((x, y) => x + y);
Print((x, y) => { int v = x * 10; return y + v; });
Console.Read();
}
}
Lambda運算式是委託的實現方法,所以必須遵循以下規則:
l Lambda運算式的參數數量必須和委託的參數數量相同;
l 如果委託的參數中包括有ref或out修飾符,則Lambda運算式的參數列中也必須包括有修飾符;
我們來看如下例子:
class Test
{
delegate void OutHandler(out int x);
static void Print(OutHandler test)
{
int i;
test(out i);
Console.Write(i);
}
static void Main()
{
Print((out int x) => x = 3);
Console.Read();
}
}
l 如果委託有傳回型別,則Lambda運算式的語句或語句塊中也必須返回相同類型的資料;
l 如果委託有幾種資料類型格式而在Lambda運算式中編譯器無法推斷具體資料類型時,則必須手動明確資料類型。
由上面可見,C# 2.0規範中提到的匿名方法規範同樣適用於Lambda運算式。Lambda運算式是匿名方法在功能行上的超集,提供了下列附加的功能:
l Lambda運算式允許省略參數類型並對其進行推斷,而匿名方法要求參數類型必須顯式地聲明。
l Lambda運算式體可以是運算式或語句塊,而匿名方法體只能是語句塊。
l 在型別參數推導和方法重載抉擇時,Lambda運算式可以被作為參數傳遞。
l 以一個運算式作為運算式體的Lambda運算式可以被轉換為運算式樹狀架構。
20.5.2 Lambda運算式轉換
和匿名方法運算式類似,Lambda運算式可以歸類為一種擁有特定轉換規則的值。這種值沒有類型,但可以被隱式地轉換為一個相容的委託類型。特別地,當滿足下列條件時,委託類型D相容於Lambda運算式L:
l D和L具有相同數量的參數;
l 如果L具有顯式類型的參數列表,D中每個參數的類型和修飾符必須和L中相應的參數完全一致;
l 如果L具有隱式類型的參數列表,則D中不能有ref或out參數;
l 如果D具有void傳回值類型,並且L的運算式體是一個運算式,若L的每個參數的類型與D的參數一致,則L的運算式體必須是一個可接受為statement-expression的有效運算式;
l 如果D具有void傳回值類型,並且L的運算式體是一個語句塊,若L的每個參數的類型與D的參數一致,則L的運算式體必須是一個有效語句塊,並且該語句塊中不能有帶有運算式的return語句;
l 如果D的傳回值類型不是void,並且L的運算式體是一個運算式,若L的每個參數的類型與D的參數一致,則L的運算式體必須是一個可以隱式轉換為D的傳回值類型的有效運算式;
l 如果D的傳回值類型不是void,並且L的運算式體是一個語句塊,若L的每個參數的類型與D的參數一致,則L的運算式體必須是一個有效語句塊,該語句塊不能有可達的終點(即必須有return語句),並且每個return語句中的運算式都必須能夠隱式轉換為D的傳回值類型。
後面的例子將使用一個範型委託F<U, T>表示一個函數,它具有一個類型為U的參數u,傳回值類型為T:
delegate T F<U, T>(U u);
我們可以像在下面這樣賦值:
F<int, int> f1 = x => x + 1;
F<int, double> f2 = x => x + 1;
每個Lambda運算式的參數和傳回值類型通過將Lambda運算式賦給的變數的類型來檢測。第一個賦值將Lambda運算式成功地轉換為了委託類型Func<int, int>,因為x的類型是int,x + 1是一個有效運算式,並且可以被隱式地轉換為int。同樣,第二個賦值成功地將Lambda運算式轉換為了委託類型Func<int, double>,因為x + 1的結果(類型為int)可以被隱式地轉換為double類型。
來看如下代碼,如果這樣賦值會怎麼樣?
F<double, int> f3 = x => x + 1;
我們運行上面的代碼,編譯器會報如下兩條錯誤:
(1)無法將類型“double”隱式轉換為“int”。存在一個顯式轉換(是否缺少強制轉換?)。
(2)無法將Lambda運算式轉換為委託類型“F<double,int>”,原因是塊中的某些傳回型別不能隱式轉換為委託傳回型別。
其實產生一個編譯期錯誤原因是,x給定的類型是double,x + 1的結果(類型為double)不能被隱式地轉換為int。
20.5.3 類型推斷
當在沒有指定型別參數的情況下調用一個範型方法時,一個類型推斷過程會去嘗試為該調用推斷型別參數。被作為參數傳遞給範型方法的Lambda運算式也會參與這個類型推斷過程。
最先發生的類型推斷獨立於所有參數。在這個初始階段,不會從作為參數的Lambda運算式推斷出任何東西。然而,在初始階段之後,將通過一個迭代過程從Lambda運算式進行推斷。特別地,當下列條件之一為真時將會完成推斷:
l 參數是一個Lambda運算式,以後簡稱為L,從其中未得到任何推斷;
l 相應參數的類型,以後簡稱為P,是一個委託類型,其傳回值類型包括了一個或多個方法型別參數;
l P和L具有相同數量的參數,P中每個參數的修飾符與L中相應的參數一致,或者如果L具有隱式類型的參數列表時,沒有參數修飾符;
l P的參數類型不包含方法型別參數,或僅包含於已經推斷出來的型別參數相相容的一群組類型參數;
l 如果L具有顯式類型的參數列表,當推斷出來的類型被P中的方法型別參數取代了時,P中的每個參數應該具有和L中相應參數一致的類型。
l 如果L具有隱式類型的參數列表,當推斷出來的類型被P中的方法型別參數取代了並且作為結果的參數類型賦給了L時,L的運算式體必須是一個有效運算式或語句塊。
l 可以為L推斷一個傳回值類型。
對於每一個這樣的參數,都是通過關聯P的傳回值類型和從L推斷出的傳回值類型來從其上進行推斷的,並且新的推斷將被添加到累積的推斷集合中。這個過程一直重複,直到無法進行更多的推斷為止。
在類型推斷和重載抉擇中,Lambda運算式L的“推斷出來的傳回值類型”通過以下步驟進行檢測:
l 如果L的運算式體是一個運算式,則該運算式的類型就是L的推斷出來的傳回值類型。
l 如果L的運算式體是一個語句塊,若由該塊中的return語句中的運算式的類型形成的集合中恰好包含一個類型,使得該集合中的每個類型都能隱式地轉換為該類型,並且該類型不是一個空類型,則該類型即是L的推斷出來的傳回值類型。
l 除此之外,無法從L推斷出一個傳回值類型。
作為包含了Lambda運算式的類型推斷的例子,請考慮System.Query.Sequence類中聲明的Select擴充方法:
namespace System.Query
{
public static class Sequence
{
public static IEnumerable<S> Select<T, S>(
this IEnumerable<T> source,
Func<T, S> selector)
{
foreach (T element in source) yield return selector(element);
}
}
}
假設使用using語句匯入了System.Query命名空間,並且定義了一個Customer類,具有一個類型為string的屬性Name,Select方法可以用於從一個Customer列表中選擇名字:
List<Customer> customers = GetCustomerList();
IEnumerable<string> names = customers.Select(c => c.Name);
對擴充方法Select的調用將被處理為一個靜態方法調用:
IEnumerable<string> names = Sequence.Select(customers, c => c.Name);
由於沒有顯式地指定型別參數,將通過類型推斷來推導型別參數。首先,customers參數被關聯到source參數,T被推斷為Customer。然後運用上面提到的拉姆達運算式類型推斷過程,C的類型是Customer,運算式c.Name將被關聯到selector參數的傳回值類型,因此推斷S是string。因此,這個調用等價於:
Sequence.Select<Customer, string>(customers, (Customer c) => c.Name);
並且其傳回值類型為IEnumerable<string>。
下面的例子示範了Lambda運算式的類型推斷是如何允許類型資訊在一個範型方法調用的參數之間“流動”的。對於給定的方法:
static Z F<X, Y, Z>(X value, Func<X, Y> f1, Func<Y, Z> f2) { return f2(f1(value)); }
現在我們來寫這樣一個調用,來看看它的推斷過程:
double seconds = F("1:15:30", s => TimeSpan.Parse(s), t => TotalSeconds);
類型推斷過程是這樣的:首先,參數"1:15:30"被關聯到value參數,推斷X為string。然後,第一個Lambda運算式的參數s具有推斷出來的類型string,運算式TimeSpan.Parse(s)被關聯到f1的傳回值類型,推斷Y是System.TimeSpan。最後,第二個Lambda運算式的參數t具有推斷出來的類型System.TimeSpan,並且運算式t.TotalSeconds被關聯到f2的傳回值類型,推斷Z為double。因此這個調用的結果類型是double。
20.5.4 重載抉擇
參數列表中的Lambda運算式將影響到特定情形下的重載抉擇(也稱重載分析,重載解析等,即從幾個重載方法中選擇最合適的方法進行調用的過程)。
下面是新添加的規則:
對於Lambda運算式L,且其具有推斷出來的傳回值類型,當委託類型D1和委託類型D2具有完全相同的參數列表,並且將L的推斷出來的傳回值類型隱式轉換為D1的傳回值類型要優於將L的推斷出來的傳回值類型隱式轉換為D2的傳回值類型時,稱L到D1的隱式轉換優於L到D2的隱式轉換。如果這些條件都不為真,則兩個轉換都不是最優的。
20.5.5 運算式樹狀架構
運算式樹狀架構允許將Lambda運算式表現為資料結構而不是可執行代碼。一個可以轉換為委託類型D的Lambda運算式,也可以轉換為一個類型為System.Linq.Expressions. Expression<D>的運算式樹狀架構。將一個Lambda運算式轉換為委託類型導致可執行代碼被委託所產生和引用,而將其轉換為一個運算式樹狀架構類型將導致建立了運算式樹狀架構執行個體的代碼被發出。運算式樹狀架構是Lambda運算式的一種高效的記憶體中資料表現形式,並且使得運算式的結構變得透明和明顯。
如下面的例子將一個Lambda運算式分別表現為了可執行代碼和運算式樹狀架構。由於存在到Func<int, int>的轉換,因此存在到Expression<Func<int, int>>的轉換。代碼如下所示:
using System.Linq.Expressions;
// 代碼
Func<int, int> f = x => x + 1;
// 資料
Expression<Func<int, int>> e = x => x + 1;
在這些賦值完成之後,委託f標識一個返回x + 1的方法,而運算式樹狀架構e表示一個描述了運算式x + 1的資料結構。