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