摘要:新Orcas語言特性:匿名型別
【原文位址】 New 「Orcas」 Language Feature: Anonymous Types
【原文發表日期】 Tuesday, May 15, 2007 7:02 AM
在過去的2個月裡,我發表了一系列文章,討論作為Visual Studio和.NET框架Orcas版本一部分發佈的一些新的語言特性。這裡是這個系列裡前4個文章的連結:
今天的文章討論我這個語言系列的最後一個新特性:匿名型別。
什麼是匿名型別(Anonymous Types)?
匿名型別是C#和VB的方便語言特性,它允許開發人員在程式碼內簡明地定義行內CLR型別,而不用明確地對型別定義一個正式的類別宣告。
匿名型別在使用LINQ做查詢,轉換/投影/構形資料時尤其有用。
匿名型別的例子
在我以前的查詢句法文章裡,我示範了你可以透過投影來轉換資料。這個LINQ的強有力的特性允許你對一個資料源(不管這個資料源是資料庫,XML文件還是記憶體中的集合)做查詢操作,然後對查詢資料的結果構形成與原先資料源不同的結構或格式。
在我以前的查詢句法文章裡,我定義了一個用來代表我轉換過後的產品資料的MyProduct類別。透過明確地定義MyProduct類別,我就有了一個正式的CLR型別契約,我可以很容易地用它來把我自訂結構的產品結果在web服務間或我的應用解決方案中的多個類別和程式集間傳遞。
但有的時候,我只想要在我當前的程式碼範圍內查詢和操作資料,我不想要另外正式地定義一個類來代表我的資料,才可以操作資料。在這種情形下,匿名型別非常有用,因為它們允許你在你的程式碼內,簡明地定義一個新型別在行內使用。
例如,假設我使用Orcas中的LINQ到SQL物件關係映射器設計器對Northwind資料庫建模,生成下列的類別:
然後我就可以使用下列程式碼來對資料庫裡的產品資料進行查詢,使用LINQ的投影/轉換功能將資料結果客制構形成與上面的Product類別有所不同的東西。但不是用一個明確定義的MyProduce類別來代表從資料庫獲取的資料行,而是用匿名型別的特性來隱含地定義一個含4個屬性的新型別來代表我客制構形的資料,像這樣:
在上面的程式碼裡,作為LINQ運算式select子句的一部分,我宣告了一個匿名型別,然後由編譯器自動生成帶4個屬性(Id, Name, UnitPrice 和 TotalRevenue)的匿名型別,這些屬性的名稱和型別是從查詢的構形中推斷出來的。
然後我使用了C#中的var這個新關鍵詞來指代從LINQ運算式傳回的這個匿名型別的 IEnumerable<T> 序列,還在後面程式碼的foreach語句裡,對這個序列進行迴圈時,用var來指代其中的每個匿名型別實例。
儘管這個句法給了我動態語言一樣的彈性,我還保留了強型別語言的好處 – 包括 Visual Studio中的編譯時檢查和程式碼intellisense支援。例如,注意上面,我是如何對傳回的產品序列做foreach的,對從LINQ查詢推斷出的帶自訂屬性的匿名型別,我還能得到完整的程式碼intellisense和編譯檢查。
理解var關鍵詞
Orcas中的C#引進了var這個新關鍵詞,在宣告局部變數時可用於替代型別名。
在第一次看見var這個新關鍵詞時,大家常有的一個錯誤認識是,這是個後期繫結或者無型別的變數引用(譬如,Object型別的引用或象Javascript中後期繫結的對象引用)。這並不正確,var關鍵詞總是生成強型別的變數引用。不是要求開發人員明確地定義變數的型別,var這個關鍵詞而是告訴編譯器在變數最先宣告時,從用來初始化變數的運算式推斷出變數的型別。
var這個關鍵詞可以用來引用C#的任何型別(意即它可用於匿名型別和明確定義的型別)。實際上,理解var這個關鍵詞的最容易的方法是看一下幾個將其用於常見明確型別的例子。譬如,我可以像下面這樣使用var這個關鍵詞來宣告三個變數:
編譯器會根據初始指派推斷出name,age和male變數的型別,在這個例子中,分別是字串,整數和布林值。這意味著,編譯器會生成與下面程式碼完全一樣的IL:
實際上,CLR根本不知道你使用了var這個關鍵詞,從它的角度來看,上面2個程式碼例子絕對沒有區別。第一個版本只不過是由編譯器提供的節省開發人員幾下鍵擊的語法糖而已,讓編譯器做苦力推斷出和宣告型別名稱。
除了使用var這個關鍵詞替代內建的資料型別外,很明顯地,你也可以將它用於你定義的任何自訂型別。例如,回到我以前部落格文章中的LINQ查詢投影例子,這個投影使用了用來資料構形的明確的MyProduct型別,我可以用var這個關鍵詞將其改寫為:
重要注意事項:雖然我在上面使用了var這個關鍵詞,我並沒將其用於匿名型別。我的LINQ查詢還是使用了MyProduct這個型別來對傳回的資料做了構形,這意味著var products宣告是IEnumerable<Product> products的速記而已。同樣地,在foreach語句中我定義的var p變數不過是MyProduct p的速記而已。
var關鍵詞的重要規則
因為var這個關鍵詞產生強型別的變數宣告,編譯器需要能夠根據它的用法推出其型別。這意味著,在用它來宣告變數時,你總是需要做個初始指派。編譯器會產生一個編譯錯誤,如果你不這麼做的話:
宣告匿名型別
至此,我們介紹了var這個關鍵詞,我們可以開始用它來指代匿名型別了。
C#中的匿名型別是使用與我語言系列第一個部落格文章裡討論過的物件初始化句法同樣的句法來定義的。其區別是,不是作為初始化語法的一部分來宣告型別名稱,而是在實體化匿名型別時,你將new關鍵詞後面的型別名稱省略掉:
編譯器會分析上面的句法,自動定義一個帶有4個屬性的新的標準CLR型別。這4個屬性的型別是根據賦給的初始值的型別來決定的。例如,在上面的例子中,Id屬性被指派了一個整數,所以編譯器將生成一個型別為整數的屬性。
匿名型別的實際CLR名稱是由C#編譯器自動生成的。CLR本身並不知道匿名型別和非匿名型別間的區別,所以兩者的運行時語意是絕對完全一樣的。Bart De Smet在這裡寫有一篇很好的部落格文章,對此做了詳細討論,如果你想知道匿名型別的確切類命名模式以及生成的IL的話。
注意上面,當你在匿名型別上鍵入」product.」 時,你依然在Visual Studio中得到編譯時檢查和完整的intellisense。還注意一下,intellisense描述是如何表明它是個AnonymousType(匿名型別)的,但依然提供了完全的屬性宣告信息,如紅線圈出的文字所示。
使用匿名型別做分層構形
匿名型別可以帶來便利的一個強有力的場景是,可以用最小量的程式碼來輕易地對資料做分層構形投影。
例如,我可以編寫下面這樣的LINQ運算式,對Northwind資料庫中價格大於50美元的所有產品進行查詢,然後將傳回的產品用以產品的ReorderLevel(庫存重訂購水平)來排序的一個分層結構來構形(使用了LINQ查詢句法支援的group into子句):
將上面的程式碼在ASP.NET中運行時,我將得到瀏覽器顯示的下列輸出:
同樣地,我也可以根據JOIN結果來做分層構形。例如,下面的程式碼生成了一個新匿名型別,它帶有幾個標準的產品欄位屬性,以及一個含有客戶對特定產品所做的最新5個訂單的分層的子集合屬性:
注意到我是如何利落地訪問分層資料的。在上面,我對產品查詢進行迴圈,然後細鑽到每個產品的最新5個訂單的。你可以看到,到處都有完整的intellisense和編譯時檢查,即使是匿名型別上訂單細節的嵌套子集合中的對象的屬性上也都有。
資料繫結匿名型別
就像我在文章前面提到的那樣,從CLR的立場來說,匿名型別和明確定義的型別間絕對毫無區別。匿名型別和var關鍵詞純屬節省程式碼量的「語法糖」,其運行時語意跟明確定義的型別是完全一樣的。
此外,這意味著,所有的標準.NET型別反射特性,對匿名型別也是工作的,即意味著,像繫結到UI控制項的特性同樣工作。例如,假如我要顯示我前面的分層LINQ查詢的結果,我可以像下面這樣在一個.aspx網頁裡定義一個 <asp:gridview> 控制項:
上面的.aspx 含有一個gridview,它有2個標準的boundfield列,一個含有嵌套的 <asp:bulletedlist> 控制項的模板欄位列,我將用這個嵌套控制項來顯示產品的分層訂單細節的子結果集。
然後,我可以編寫下面這個LINQ程式碼來對資料庫做一個分層查詢,然後將客制構形的資料結果繫結到GridView來顯示:
因為GridView支援對任何 IEnumerable<T> 序列的繫結,使用反射獲取屬性值,它對我上面使用的匿名型別依然工作。
在運行時,上面的程式碼會產生一個產品細節以及產品最新的訂單數量的分層列表的簡單網格,像這樣:
很明顯地,你可以把這個報表做得更豐富,更漂亮,但希望你能從中瞭解到,現在對資料庫做分層查詢是多麼的容易,可以對傳回的結果做你要的構形,然後對結果用程式設計手法操作或將其繫結到UI控制項上。
結語
匿名型別是個很方便的語言特性,允許開發人員在程式碼內簡明地定義行內CLR型別,而不用提供一個正式的類定義宣告。雖然它們可以在很多場合下使用,但在使用LINQ查詢和轉換/構形資料時尤其有用。
這個文章結束了我5個部分的Orcas語言系列。以後,我會發表更多的LINQ文章,來示範如何在實戰上使用所有這些新語言特性來做常見的資料存取操作(定義資料模型,查詢,更新,使用預存程式,驗證等等)。但我想先把這個5個部分的語言系列完成,這樣我們在我將來的文章裡深入探討時,你才會真正理解所用的底層語言構造。
希望本文對你有所幫助,
Scott