LINQ 的演變及其對 C# 設計的影響

來源:互聯網
上載者:User
我曾是 Connections
系列節目的一名超級愛好者,這是在《探索頻道》(Discovery Channel) 中由 James Burke
主持的節目。其基本假定是:看起來毫不相關的發現是如何影響其他發現,而這些發現最終又為現代生活提供了便利。其寓意是,如果您想進步,任何進步都不是孤
立地取得的。Language-integrated Query (LINQ) (LINQ) 也是如此,這毫不奇怪。


單地說,LINQ 是支援以型別安全方式查詢資料的一系列語言擴充;它將在代號為“Orcas”的下一個版本 Visual Studio
中發布。待查詢資料的形式可以是 XML(LINQ 到 XML)、資料庫(啟用 LINQ 的 ADO.NET,其中包括 LINQ 到
SQL、LINQ 到 Dataset 和 LINQ 到 Entities)和對象 (LINQ 到 Objects) 等。LINQ 體繫結構如圖 1 所示。


圖 1 LINQ 體繫結構 (單擊該映像獲得較小視圖)

圖 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<Customer> locals = customers.where(ZipCode == 98112);

在此例中,Sequence 是
IEnumerable<T> 的別名;“where”一詞是編譯器能理解的一種特殊運算子。Where 運算子的實現是一種接受
predicate 委託(即 bool Pred<T>(T item) 形式的委託)的普通 C#
靜態方法。本構思的目的是讓編輯器具備與運算子有關的特殊知識。這樣將允許編譯器正確調用靜態方法並建立代碼,將委託與運算式聯絡起來。

假設上述樣本是 C# 的理想查詢文法。在沒有任何語言擴充的情況下,該查詢在 C# 2.0 中又會是什麼樣子?

IEnumerable<Customer> locals = EnumerableExtensions.Where(customers,
delegate(Customer c)
{
return c.ZipCode == 98112;
});

這個代碼驚人地冗長,而且更糟糕的是,需
要非常仔細地研究才能找到相關的篩選器 (ZipCode ==
98112)。這隻是一個簡單的例子;試想一下,如果使用數個篩選器、投影等,要讀懂代碼該有多難。冗長的根源在於匿名方法所要求的文法。在理想的查詢
中,除了要計算的運算式,運算式不會提出任何要求。隨後,編譯器將嘗試推斷上下文;例如,ZipCode 實際上引用了 Customer 上定義的
ZipCode。如何解決這一問題?將特定運算子的知識寫入程式碼到語言中並不能令語言設計團隊滿意,因此他們開始為匿名方法尋求替代文法。他們要求該文法應
極其簡練,但又不必比匿名方法當前所需的編譯器要求更多的知識。最終,他們發明了 lambda 運算式。

Lambda
運算式是一種語言功能,在許多方面類似於匿名方法。事實上,如果 lambda
運算式首先被引入語言,那麼就不會有對匿名方法的需要了。這裡的基本概念是可以將代碼視為資料。在 C# 1.0
中,通常可以將字串、整數、參考型別等傳遞給方法,以便方法對那些值進行操作。匿名方法和 lambda
運算式擴充了值的範圍,以包含代碼塊。此概念常見於函數式編程中。

我們再借用以上樣本,並用 lambda 運算式替換匿名方法:

IEnumerable<Customer> locals = 
EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);

有幾個需要注意的地方。對於初學者而言,
lambda 運算式簡明扼要的原因有很多。首先,沒有使用委託關鍵字來引入構造。取而代之的是一個新的運算子
=>,通知編譯器這不是Regex。其次,Customer 類型是從使用中推斷出來的。在此例中,Where 方法的簽名如下所示:

public static IEnumerable<T> Where<T>(
IEnumerable<T> items, Func<T, bool> predicate)

編譯器能夠推斷“c”是指客戶,因為
Where 方法的第一個參數是 IEnumerable<Customer>,因此 T 事實上必須是
Customer。利用這種知識,編譯器還可驗證 Customer 具有一個 ZipCode
成員。最後,沒有指定的返回關鍵字。在文法形式中,返回成員被省略,但這隻是為了文法便利。運算式的結果仍將視為傳回值。

與匿名方法一樣,Lambda 運算式也支援變數捕獲。例如,對於在 lambda 運算式主體內包含 lambda 運算式的方法,可以引用其參數或局部變數:

public IEnumerable<Customer> LocalCusts(
IEnumerable<Customer> 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<Customer> locals = 
EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);

這裡存在一個明顯的問題。客戶目前必須瞭解此 EnumerableExtensions 類,而不是考慮可在 Customer 上執行的操作。另外,在多個運算子的情況下,使用者必須逆轉其思維以編寫正確的文法。例如:

IEnumerable<string> locals = 
EnumerableExtensions.Select(
EnumerableExtensions.Where(customers, c => c.ZipCode == 91822),
c => c.Name);

請注意,Select 屬於外部方法,儘管它是在 Where 方法結果的基礎上啟動並執行。理想的文法應該更類似以下代碼:

sequence<Customer> locals = 
customers.where(ZipCode == 98112).select(Name);

因此,是否可利用另一種語言功能來進一步接近實現理想文法呢?

擴充方法


果證明,更好的文法將以被稱為擴充方法的語言功能形式出現。擴充方法基本上屬於可通過執行個體文法調用的靜態方法。上述查詢問題的根源是我們試圖向
IEnumerable<T> 添加方法。但如果我們要添加運算子,如 Where、Select
等,則所有現有和未來的實現器都必須實現那些方法。儘管那些實現絕大多數都是相同的。在 C#
中共用“介面實現”的唯一方法是使用靜態方法,這是我們處理以前使用的 EnumerableExtensions 類的一個成功方法。

假設我們轉而將 Where 方法編寫為擴充方法。那麼,查詢可重新編寫為:

IEnumerable<Customer> locals = 
customers.Where(c => c.ZipCode == 91822);

對於此簡單查詢,該文法近乎完美。但將 Where 方法編寫為擴充方法的真正含義是什麼呢?其實非常簡單。基本上,因為靜態方法的簽名發生更改,因此“this”修飾符就被添加到第一個參數:

public static IEnumerable<T> Where<T>(
this IEnumerable<T> items, Func<T, bool> predicate)

此外,必須在靜態類中聲明該方法。靜態類
是一種只能包含靜態成員,並在類聲明中用靜態修飾符表示的類。這就它的全部含義。此聲明指示編譯器允許在任何實現
IEnumerable<T> 的類型上用與執行個體方法相同的文法調用 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<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 語句中返回客戶名稱。

當投影只是單一欄位時,該方法確實很有效。但是,假設我們不僅要返回客戶的名稱,還要返回客戶的地址。理想的文法則應如下所示:

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<CustomerTuple> 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, List<Customer>>();

現在我們的查詢取得了良好進展;我們已經接近理想的文法,而且我們是用通用語言功能來達成的。

有趣的是,我們發現,隨著越來越多的人使用過此文法,經常會出現允許投影超越方法邊界的需求。如我們以前所看到的,這是可能的,只要從 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 背景的開發人員閱讀和理解。

作者簡介:

Anson Horton曾在 Microsoft 擔任專案經理將近六年。自 C#
團隊成立以來,他一直是其中的一員,此前他是 C++ 團隊的成員。他曾經參與設計 C# 語言和編譯器、C# 項目系統、C# IDE
(IntelliSense) 和 C# Expression 計算機和調試器。Anson 在 blogs.msdn.com/ansonh 擁有一個部落格,他很少更新其中的內容。

原文:http://msdn.microsoft.com/msdnmag/issues/07/06/CSharp30/Default.aspx?loc=zh

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.