簡單地說,LINQ 是支持以類型安全方式查詢數(shù)據(jù)的一系列語言擴展;它將在代號為“Orcas”的下一個版本 Visual Studio 中發(fā)布。待查詢數(shù)據(jù)的形式可以是 XML(LINQ 到 XML)、數(shù)據(jù)庫(啟用 LINQ 的 ADO.NET,其中包括 LINQ 到 SQL、LINQ 到 Dataset 和 LINQ 到 Entities)和對象 (LINQ 到 Objects) 等。LINQ 體系結(jié)構(gòu)如圖 1 所示。
讓我們看一些代碼。在即將發(fā)布的“Orcas”版 C# 中,LINQ 查詢可能如下所示:
var overdrawnQuery = from account in db.Accounts where account.Balance < 0 select new { account.Name, account.Address };
當使用 foreach 遍歷此查詢的結(jié)果時,返回的每個元素都將包含一個余額小于 0 的帳戶的名稱和地址。
從以上示例中立即可以看出該語法類似于 SQL。幾年前,Anders Hejlsberg(C# 的首席設(shè)計師)和 Peter Golde 曾考慮擴展 C# 以更好地集成數(shù)據(jù)查詢。Peter 時任 C# 編譯器開發(fā)主管,當時正在研究擴展 C# 編譯器的可能性,特別是支持可驗證 SQL 之類特定于域的語言語法的加載項。另一方面,Anders 則在設(shè)想更深入、更特定級別的集成。他當時正在構(gòu)思一組“序列運算符”,能在實現(xiàn) IEnumerable 的任何集合以及實現(xiàn) IQueryable 的遠程類型查詢上運行。最終,序列運算符的構(gòu)思獲得了大多數(shù)支持,并且 Anders 于 2004 年初向比爾·蓋茨的 Thinkweek 遞交了一份關(guān)于本構(gòu)思的文件。反饋對此給予了充分肯定。在設(shè)計初期,簡單查詢的語法如下所示:
sequence<Customer> locals = customers.where(ZipCode == 98112);
在此例中,Sequence 是 IEnumerable<T> 的別名;“where”一詞是編譯器能理解的一種特殊運算符。Where 運算符的實現(xiàn)是一種接受 predicate 委托(即 bool Pred<T>(T item) 形式的委托)的普通 C# 靜態(tài)方法。本構(gòu)思的目的是讓編輯器具備與運算符有關(guān)的特殊知識。這樣將允許編譯器正確調(diào)用靜態(tài)方法并創(chuàng)建代碼,將委托與表達式聯(lián)系起來。
假設(shè)上述示例是 C# 的理想查詢語法。在沒有任何語言擴展的情況下,該查詢在 C# 2.0 中又會是什么樣子?
IEnumerable<Customer> locals = EnumerableExtensions.Where(customers, delegate(Customer c) { return c.ZipCode == 98112; });
這個代碼驚人地冗長,而且更糟糕的是,需要非常仔細地研究才能找到相關(guān)的篩選器 (ZipCode == 98112)。這只是一個簡單的例子;試想一下,如果使用數(shù)個篩選器、投影等,要讀懂代碼該有多難。冗長的根源在于匿名方法所要求的語法。在理想的查詢中,除了要計算的表達式,表達式不會提出任何要求。隨后,編譯器將嘗試推斷上下文;例如,ZipCode 實際上引用了 Customer 上定義的 ZipCode。如何解決這一問題?將特定運算符的知識硬編碼到語言中并不能令語言設(shè)計團隊滿意,因此他們開始為匿名方法尋求替代語法。他們要求該語法應極其簡練,但又不必比匿名方法當前所需的編譯器要求更多的知識。最終,他們發(fā)明了 lambda 表達式。
Lambda 表達式
Lambda 表達式是一種語言功能,在許多方面類似于匿名方法。事實上,如果 lambda 表達式首先被引入語言,那么就不會有對匿名方法的需要了。這里的基本概念是可以將代碼視為數(shù)據(jù)。在 C# 1.0 中,通??梢詫⒆址⒄麛?shù)、引用類型等傳遞給方法,以便方法對那些值進行操作。匿名方法和 lambda 表達式擴展了值的范圍,以包含代碼塊。此概念常見于函數(shù)式編程中。
我們再借用以上示例,并用 lambda 表達式替換匿名方法:
IEnumerable<Customer> locals = EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);
有幾個需要注意的地方。對于初學者而言,lambda 表達式簡明扼要的原因有很多。首先,沒有使用委托關(guān)鍵字來引入構(gòu)造。取而代之的是一個新的運算符 =>,通知編譯器這不是正則表達式。其次,Customer 類型是從使用中推斷出來的。在此例中,Where 方法的簽名如下所示:
public static IEnumerable<T> Where<T>( IEnumerable<T> items, Func<T, bool> predicate)
編譯器能夠推斷“c”是指客戶,因為 Where 方法的第一個參數(shù)是 IEnumerable<Customer>,因此 T 事實上必須是 Customer。利用這種知識,編譯器還可驗證 Customer 具有一個 ZipCode 成員。最后,沒有指定的返回關(guān)鍵字。在語法形式中,返回成員被省略,但這只是為了語法便利。表達式的結(jié)果仍將視為返回值。
與匿名方法一樣,Lambda 表達式也支持變量捕獲。例如,對于在 lambda 表達式主體內(nèi)包含 lambda 表達式的方法,可以引用其參數(shù)或局部變量:
public IEnumerable<Customer> LocalCusts( IEnumerable<Customer> customers, int zipCode) { return EnumerableExtensions.Where(customers, c => c.ZipCode == zipCode); }
最后,Lambda 表達式支持更冗長的語法,允許您顯式指定類型,以及執(zhí)行多條語句。例如:
return EnumerableExtensions.Where(customers, (Customer c) => { int zip = zipCode; return c.ZipCode == zip; });
好消息是,我們向原始文章中提議的理想語法邁進了一大步,并且我們能夠利用一個通常能在查詢運算符以外發(fā)揮作用的語言功能來實現(xiàn)這一目標。讓我們再次看一下我們目前所處的階段:
IEnumerable<Customer> locals = EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);
這里存在一個明顯的問題??蛻裟壳氨仨毩私獯?EnumerableExtensions 類,而不是考慮可在 Customer 上執(zhí)行的操作。另外,在多個運算符的情況下,使用者必須逆轉(zhuǎn)其思維以編寫正確的語法。例如:
IEnumerable<string> locals = EnumerableExtensions.Select( EnumerableExtensions.Where(customers, c => c.ZipCode == 91822), c => c.Name);
請注意,Select 屬于外部方法,盡管它是在 Where 方法結(jié)果的基礎(chǔ)上運行的。理想的語法應該更類似以下代碼:
sequence<Customer> locals = customers.where(ZipCode == 98112).select(Name);
因此,是否可利用另一種語言功能來進一步接近實現(xiàn)理想語法呢?
擴展方法
結(jié)果證明,更好的語法將以被稱為擴展方法的語言功能形式出現(xiàn)。擴展方法基本上屬于可通過實例語法調(diào)用的靜態(tài)方法。上述查詢問題的根源是我們試圖向 IEnumerable<T> 添加方法。但如果我們要添加運算符,如 Where、Select 等,則所有現(xiàn)有和未來的實現(xiàn)器都必須實現(xiàn)那些方法。盡管那些實現(xiàn)絕大多數(shù)都是相同的。在 C# 中共享“接口實現(xiàn)”的唯一方法是使用靜態(tài)方法,這是我們處理以前使用的 EnumerableExtensions 類的一個成功方法。
假設(shè)我們轉(zhuǎn)而將 Where 方法編寫為擴展方法。那么,查詢可重新編寫為:
IEnumerable<Customer> locals = customers.Where(c => c.ZipCode == 91822);
對于此簡單查詢,該語法近乎完美。但將 Where 方法編寫為擴展方法的真正含義是什么呢?其實非常簡單?;旧?,因為靜態(tài)方法的簽名發(fā)生更改,因此“this”修飾符就被添加到第一個參數(shù):
public static IEnumerable<T> Where<T>( this IEnumerable<T> items, Func<T, bool> predicate)
此外,必須在靜態(tài)類中聲明該方法。靜態(tài)類是一種只能包含靜態(tài)成員,并在類聲明中用靜態(tài)修飾符表示的類。這就它的全部含義。此聲明指示編譯器允許在任何實現(xiàn) IEnumerable<T> 的類型上用與實例方法相同的語法調(diào)用 Where。但是,必須能夠從當前作用域訪問 Where 方法。當包含類型處于作用域內(nèi)時,方法也在作用域內(nèi)。因此,可以通過 Using 指令將擴展方法引入作用域。(有關(guān)詳細信息,請參見側(cè)欄上的“擴展方法”。)
顯然,擴展 方法有助于簡化我們的查詢示例,但除此之外,這些方法是不是一種廣泛有用的語言功能呢?事實證明擴展方法有多種用途。其中一個最常見的用途可能是提供共享接口實現(xiàn)。例如,假設(shè)您有以下接口:
interface IDog { // Barks for 2 seconds void Bark(); void Bark(int seconds); }
此接口要求每個實現(xiàn)器都應編寫適用于兩種重載的實現(xiàn)。有了“Orcas”版 C#,接口變得很簡單:
interface IDog { void Bark(int seconds); }
擴展方法可添加到另一個類:
static class DogExtensions { // Barks for 2 seconds public static void Bark(this IDog dog) { dog.Bark(2); } }
接口實現(xiàn)器現(xiàn)在只需實現(xiàn)單一方法,但接口客戶端卻可以自由調(diào)用任一重載。
我們現(xiàn)在擁有了用于編寫篩選子句的非常接近理想的語法,但“Orcas”版 C# 僅限于此嗎?并不全然。讓我們對示例稍作擴展,相對于整個客戶對象,我們只投影出客戶名稱。如我前面所述,理想的語法應采用如下形式:
sequence<string> locals = customers.where(ZipCode == 98112).select(Name);
僅用我們討論過的語言擴展,即 lambda 表達式和擴展方法,此代碼可重新編寫為如下所示:
IEnumerable<string> locals = customers.Where(c => c.ZipCode == 91822).Select(c => c.Name);
請注意,此查詢的返回類型不同,它是 IEnumerable<string> 而不是 IEnumerable<Customer>。這是因為我們僅從 select 語句中返回客戶名稱。
當投影只是單一字段時,該方法確實很有效。但是,假設(shè)我們不僅要返回客戶的名稱,還要返回客戶的地址。理想的語法則應如下所示:
locals = customers.where(ZipCode == 98112).select(Name, Address);
匿名類型
如果我們想繼續(xù)使用我們現(xiàn)有的語法來返回名稱和地址,我們很快便會面臨問題,即不存在僅包含 Name 和 Address 的類型。雖然我們?nèi)匀豢梢跃帉懘瞬樵儯潜仨氁朐擃愋停?/p>
class CustomerTuple { public string Name; public string Address; public CustomerTuple(string name, string address) { this.Name = name; this.Address = address; } }
然后我們才能使用該類型,即此處的 CustomerTuple,以生成我們查詢的結(jié)果。
IEnumerable<CustomerTuple> locals = customers.Where(c => c.ZipCode == 91822) .Select(c => new CustomerTuple(c.Name, c.Address));
那確實像許多用于投影出字段子集的樣板代碼。而且還往往不清楚如何命名此種類型。CustomerTuple 確實是個好名稱嗎?如果投影出 Name 和 Age 又該如何命名?那也可以叫做 CustomerTuple。因此,問題在于我們擁有樣板代碼,而且似乎無法為我們創(chuàng)建的類型找到任何恰當?shù)拿Q。此外,還可能需要許多不同的類型,如何管理這些類型很快便可能成為一個棘手的問題。
這正是匿名類型要解決的問題。此功能主要允許在無需指定名稱的情況下創(chuàng)建結(jié)構(gòu)化類型。如果我們使用匿名類型重新編寫上述查詢,其代碼如下所示:
locals = customers.Where(c => c.ZipCode == 91822) .Select(c => new { c.Name, c.Address });
此代碼會隱式創(chuàng)建一個具有 Name 和 Address 字段的類型:
class { public string Name; public string Address; }
此類型不能通過名稱引用,因為它沒有名稱。創(chuàng)建匿名類型時,可顯式聲明字段的名稱。例如,如果正在創(chuàng)建的字段派生于一條復雜的表達式,或純粹不需要名稱,就可以更改名稱:
locals = customers.Where(c => c.ZipCode == 91822) .Select(c => new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address });
在此情形下,生成的類型具有名為 FullName 和 HomeAddress 的字段。
這樣我們又向理想世界前進了一步,但仍存在一個問題。您將發(fā)現(xiàn),我在任何使用匿名類型的地方都策略性地省略了局部變量的類型。顯然我們不能聲明匿名類型的名稱,那我們?nèi)绾问褂盟鼈儯?/p>
隱式類型化部變量
還有另一種語言功能被稱為隱式類型化局部變量(或簡稱為 var),它負責指示編譯器推斷局部變量的類型。例如:
var integer = 1;
在此例中,整數(shù)具有 int 類型。請務必明白,這仍然是強類型。在動態(tài)語言中,整數(shù)的類型可在以后更改。為說明這一點,以下代碼不會成功編譯:
var integer = 1; integer = “hello”;
C# 編譯器將報告第二行的錯誤,表明無法將字符串隱式轉(zhuǎn)換為 int。
在上述查詢示例中,我們現(xiàn)在可以編寫完整的賦值,如下所示:
var locals = customers .Where(c => c.ZipCode == 91822) .Select(c => new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address });
局部變量的類型最終成為 IEnumerable<?>,其中“?”是無法編寫的類型的名稱(因為它是匿名的)。
隱式類型化局部變量只是:方法內(nèi)部的局部變量。它們無法超出方法、屬性、索引器或其他塊的邊界,因為該類型無法顯式聲明,而且“var”對于字段或參數(shù)類型而言是非法的。
事實證明,隱式類型化局部變量在查詢的環(huán)境之外非常便利。例如,它有助于簡化復雜的通用實例化:
var customerListLookup = new Dictionary<string, List<Customer>>();
現(xiàn)在我們的查詢?nèi)〉昧肆己眠M展;我們已經(jīng)接近理想的語法,而且我們是用通用語言功能來達成的。
有趣的是,我們發(fā)現(xiàn),隨著越來越多的人使用過此語法,經(jīng)常會出現(xiàn)允許投影超越方法邊界的需求。如我們以前所看到的,這是可能的,只要從 Select 內(nèi)部調(diào)用對象的構(gòu)造函數(shù)來構(gòu)建對象即可。但是,如果沒有用來準確接受您需要設(shè)置的值的構(gòu)造函數(shù),會發(fā)生什么呢?
對象初始值
為解決這一問題,即將發(fā)布的“Orcas”版本提供了一種被稱為對象初始值的 C# 語言功能。對象初始值主要允許在單一表達式中為多個屬性或字段賦值。例如,創(chuàng)建對象的常見模式是:
Customer customer = new Customer(); customer.Name = “Roger”; customer.Address = “1 Wilco Way”;
此時,Customer 沒有可以接受名稱和地址的構(gòu)造函數(shù);但是存在兩個屬性,即 Name 和 Address,當創(chuàng)建實例后即可設(shè)置它們。對象初始值允許使用以下語法創(chuàng)建相同的結(jié)果:
Customer customer = new Customer() { Name = “Roger”, Address = “1 Wilco Way” };
在我們前面的 CustomerTuple 示例中,我們通過調(diào)用其構(gòu)造函數(shù)創(chuàng)建了 CustomerTuple 類。我們也可以通過對象初始值獲得同樣的結(jié)果:
var locals = customers .Where(c => c.ZipCode == 91822) .Select(c => new CustomerTuple { Name = c.Name, Address = c.Address });
請注意,對象初始值允許省略構(gòu)造函數(shù)的括號。此外,字段和可設(shè)置的屬性均可在對象初始值的主體內(nèi)部進行賦值。
我們現(xiàn)在已經(jīng)擁有在 C# 中創(chuàng)建查詢的簡潔語法。盡管如此,我們還有一種可擴展途徑,可通過擴展方法以及一組本身非常有用的語言功能來添加新的運算符(Distinct、OrderBy、Sum 等)。
語言設(shè)計團隊現(xiàn)在有了數(shù)種可賴以獲得反饋的原型。因此,我們與許多富于 C# 和 SQL 經(jīng)驗的參與者組織了一項可用性研究。幾乎所有反饋都是肯定的,但明顯疏忽了某些東西。具體而言,開發(fā)人員難以應用他們的 SQL 知識,因為我們認為理想的語法與他們擅長領(lǐng)域的專門技術(shù)并不很符合。
查詢表達式
于是,語言設(shè)計團隊設(shè)計了一種與 SQL 更為相近的語法,稱為查詢表達式。例如,針對我們的示例的查詢表達式可如下所示:
var locals = from c in customers where c.ZipCode == 91822 select new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address };
查詢表達式是基于上述語言功能構(gòu)建而成。它們在語法上,完全轉(zhuǎn)換為我們已經(jīng)看到的基礎(chǔ)語法。例如,上述查詢可直接轉(zhuǎn)換為:
var locals = customers .Where(c => c.ZipCode == 91822) .Select(c => new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address });
查詢表達式支持許多不同的“子句”,如 from、where、select、orderby、group by、let 和 join。這些子句先轉(zhuǎn)換為對等的運算符調(diào)用,后者進而通過擴展方法實現(xiàn)。如果查詢語法不支持必要運算符的子句,則查詢子句和實現(xiàn)運算符的擴展方法之間的緊密關(guān)系很便于將兩者結(jié)合。例如:
var locals = (from c in customers where c.ZipCode == 91822 select new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address}) .Count();
在本例中,查詢現(xiàn)在返回在 91822 ZIP Code 區(qū)居住的客戶人數(shù)。
通過該種方法,我們已經(jīng)設(shè)法在結(jié)束時達到了開始時的目標(我對這一點始終覺得非常滿意)。下一版本的 C# 的語法歷經(jīng)數(shù)年時間的發(fā)展,嘗試了許多新的語言功能,才最終到達近乎于 2004 年冬提議的原始語法的境界。查詢表達式的加入以 C# 即將發(fā)布的版本的其他語言功能為基礎(chǔ),并促使許多查詢情況更便于具有 SQL 背景的開發(fā)人員閱讀和理解。