摘要:新Orcas語言特性:擴充方法
【原文位址】New 「Orcas」 Language Feature: Extension Methods
【原文發表日期】 Tuesday, March 13, 2007 2:27 AM
上個星期,我發表了我準備寫的討論一些新的VB和C#語言特性的系列部落格文章的第一篇,這些新語言特性是將於今年晚些時候發佈的Visual Studio和.NET框架Orcas版的一部分。
我的上一個部落格文章討論了自動屬性,物件初始化器和集合初始化器等新特性。如果你還沒有讀過這個文章的話,請在這裡閱讀。今天的文章討論一個VB和C#中都具有的,重要得多的新特性:擴充方法 (Extension Methods)。
什麼是擴充方法 (Extension Methods)?
擴充方法允許開發人員往一個現有的CLR型別的公開契約(contract)中添加新的方法,而不用生成子類別或者重新編譯原來的型別。擴充方法有助於把今天動態語言中流行的對duck typing的支援之彈性,與強型別語言之性能和編譯時驗證融合起來。
擴充方法促成了好多有用的使用場景,並使在作為Orcas一部分發佈的.NET版本中引進的非常強大的LINQ查詢框架成為可能。
簡單的擴充方法例子:
有沒有想過要檢查一個字串變數是否是個合法的電子郵件位址? 在今天,你大概需要透過叫用一個單獨的類別(或許透過一個靜態方法)來實現檢查該字串變數是否合法。譬如,像這樣:
if ( EmailValidator.IsValid(email) ) {
}
而使用C#和VB中的新「擴充方法」語言特性的話,我則可以添加一個有用的「IsValidEmailAddress()」方法到string類別本身中去,該方法傳回當前字串實例是否是個合法的字串。然後我可以把我的程式碼覆寫一下,使之更加乾淨,而且更具描述性,像這樣:
if ( email.IsValidEmailAddress() ) {
}
我們是怎麼把這個新的IsValidEmailAddress()方法添加到現有的string類別裡去的呢?我們是透過定義一個靜態的型別,帶有我們的「IsValidEmailAddress」這個靜態的方法來實現的,像下面這樣:
{
public static bool IsValidEmailAddress(this string s)
{
Regex regex = new Regex(@」^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$」);
return regex.IsMatch(s);
}
}
注意,上面的靜態方法在第一個型別是string的參數變數前有個「this」關鍵詞,這告訴編譯器,這個特定的擴充方法應該添加到型別為「string」的物件中去。然後在IsValidEmailAddress()方法實現裡,我可以存取叫用該方法的實際string實例的所有公開屬性/方法/事件,取決於它是否是合法電子郵件位址來傳回true/false。
在我的程式碼裡把這個特定的擴充方法的實現添加到string實例,我只要使用標準的「using」語句來引入含有該擴充方法的實現的命名空間:
然後編譯器就會在任何string上正確地定位IsValidEmailAddress()方法。在公開發行的Orcas三月份的CTP中的C#和VB在Visual Studio程式碼編輯器裡對擴充方法提供了完整的intellisense支援。所以,當我在一個字串變數上點擊「.」關鍵詞時,我的擴充方法現在就會出現在intellisense的下拉框裡:
VB和C#編譯器也會很自然地給與你對所有擴充方法用法的編譯時的檢查,這意味著你會得到一個編譯時錯誤,假如你鍵錯或者錯用一個擴充方法的話。
[感謝David Hayden,是他在去年的一個老文章 裡第一個示範了我在上面使用的這個IsValidEmailAddress使用場景。]
擴充方法使用場景續…
利用擴充方法這個新特性來給個別型別添加方法給開發人員開闢了許多有用的擴充性使用場景。但使得擴充方法非常強有力的是,它們不僅能夠應用到個別型別上,也能應用到.NET框架中任何基類或介面上。這允許開發人員建立種種可用於整個.NET框架的豐富的可組合的框架層擴充。
譬如,考慮這樣一個場景,我想要一個既容易,描述性又強的方式來檢查一個物件是否已經包含在一個物件集合或陣列裡。我可以定義一個簡單的.In(集合)擴充方法,我想把它添加到.NET框架中的所有物件上,我可以在C#裡這麼來實現這個「In()」擴充方法:
注意上面我是如何宣告擴充方法的第一個參數的:「this object o」。這表明,這個擴充方法應該適用於繼承於基礎類別System.Object的所有型別,這意味著我可以在.NET中的每個物件上使用它。
上面這個「In」方法的實現允許我檢查一個指定的物件是否包含在作為方法參數傳入的一個IEnumerable序列中。因為所有的.NET集合和陣列都實現了IEnumerable介面,現在我擁有了一個有用的,描述性強的方法來檢查一個任意的物件是否屬於任何.NET集合或陣列。
然後我就可以使用這個「In()」方法來看一個特定的字串是否在一個字串陣列中:
我也可以用它來檢查一個特定的ASP.NET控制項是否在一個容器控制項集合裡:
我甚至可以將其用在像整數這樣的數值資料型別上:
注意上面,你甚至可以在像整數值42這樣的基本資料型別值上使用擴充方法。因為CLR支援數實值型別的自動boxing/unboxing,擴充方法可以直接使用在數值和其他數值資料型別上。
你大概可以開始從上面的例子中看出,擴充方法可以促成一些非常豐富和描述性強的擴充性使用場景。當使用於.NET中常見的基類和介面上時,他們可以促成一些非常好的特定於某個領域(domain specific)的框架和組合使用場景。
內建的System.Linq擴充方法
一個在Orcas時段隨.NET發佈的內建的擴充方法庫是一套允許開發人員對任何資料進行查詢的非常強有力的查詢擴充方法。這些擴充方法實現位於新的 System.Linq 命名空間之下,定義了標準的查詢運算子擴充方法,可以為.NET開發人員用來輕鬆地查詢XML,關聯式資料庫,.NET 物件, 和任何其他資料結構型別。
下面是使用這些查詢擴充方法的擴充性模型的幾個好處:
1) 它允許一個可用於所有資料型別(資料庫,XML文件,記憶體中的物件,以及web-services等)的共同的查詢程式設計模型和語法。
2) 它是可以組合的,允許開發人員輕鬆地往查詢語法中添加新的方法/運算子。譬如,我們可以將我們自訂的「In()」方法與為LINQ所定義的標準的「Where()」方法作為一個單獨查詢的一部分一起使用。我們自訂的In()方法看上去就跟由System.Linq命名空間提供的標準方法一樣。
3) 它是可擴充的,允許與任何資料提供器型別一起使用。譬如,任何一個像NHibernate或LLBLGen這樣現有的ORM引擎可以實現LINQ的標準查詢運算子來允許對他們現有的ORM實現和映射引擎實現LINQ查詢。這允許開發人員學會一個查詢資料的共同方式,然後對種類繁多的豐富資料存儲實現使用同樣的技能。
我將在下幾個星期裡對LINQ作更多的示範,但想留給你幾個例子,這些例子展示了如何對不同型別的資料使用幾個內建的LINQ查詢擴充方法:
使用場景一:對記憶體中的.NET物件使用LINQ擴充方法
假定我們像這樣定義了代表「Person」的類:
然後我可以使用新的物件初始化器和集合初始化器特性建立和填充一個「people」集合,像這樣:
然後我可以使用由System.Linq提供的標準的「Where()」擴充方法來獲取這個集合中FirstName的首字元是」S」的那些「Person」物件,像這樣:
上面這個新的 p => 語法是「Lambda運算式」的一個例子,是對C# 2.0匿名方法支援的更簡明的發展,允許我們透過一個實參來輕鬆地表達查詢過濾(在這個情形下,我們表示我們只想要傳回一串firstname屬性的首字元是「S」字母的Person物件) 。上面這個查詢然後就會傳回包含2個物件的序列,Scott 和 Susanne。
我也可以利用由System.Linq提供的新的「Average」 和「Max」擴充方法編寫程式碼來決定我的集合裡的人的平均年齡,以及年齡最大的人,像這樣:
使用場景二:對XML文件使用LINQ擴充方法
你手工在記憶體裡建立一個硬寫(hard-coded)的資料集合大概是很少見的。更有可能的是,你會從一個XM文件,資料庫,或web服務裡獲取資料。
假定我們在硬碟上有一個XML文件,包含下面這樣的資料:
很明顯地,我可以使用現有的 System.Xml APIs 來載入這個XML文件進一個DOM,然後存取它,或者使用一個層次較低的XmlReader API ,自己對之手工分析。或者,在 Orcas中,我現在也可以使用支援標準的LINQ擴充方法的System.Xml.Linq 實現(即 XLINQ),更優雅地分析和處理XML。
下面的程式碼例子展示了如何使用LINQ來獲取所有包含一個子節點的值的首字母為「S」的<person> XML元素:
注意,它使用了跟記憶體中物件例子中一模一樣的 Where() 擴充方法。現在它傳回一個「XElement」元素序列,XElemen是沒有型別的XML節點元素。或者我也可以覆寫查詢運算式,透過LINQ的 Select() 擴充方法來構造資料形狀,提供一個使用了新的物件初始化器句法的Lambda 運算式來填充同樣的「Person」類別,跟我們第一個記憶體中的集合的例子一樣:
上面的程式碼會做需要打開,分析,和過濾XML,然後傳回一個強型別的Person物件序列所有的工作,不需要什麼映射或持久的文件來映射數值,我只是在上面的LINQ查詢式裡直接指明了從XML到物件的構形而已。
我也可以和前面一樣使用同樣的Average() 和 Max() LINQ擴充方法來計算XML文件中<person>元素的平均年齡,以及最大年齡,像這樣:
我不用手工分析XML文件,XLINQ 不僅可以為我處理分析,它在估算LINQ運算式時,也可以使用低層的XMLReader,而不是使用DOM來分析文件。這意味著它是迅速之極,而且不配置很多記憶體。
使用場景三:對資料庫使用LINQ擴充方法
假定我們擁有一個SQL資料庫,內含一個叫「People」的表格,具有下列資料定義:
我可以使用Visual Studio中新的LINQ到SQL的所見即所得(WYSIWYG) ORM設計器,快速地建立一個映射到資料庫的「Person」類別:
然後我可以使用我先前用於物件和XML文件同樣的LINQ Where() 擴充方法,從資料庫中獲取firstname的首字元為「S」的強型別「Person」物件序列:
注意,查詢句法與物件和XML場景中的一模一樣。
然後我也可以使用與前
面一樣的 LINQ Average() 和Max() 擴充方法來從資料庫裡獲取平均和最大值,像這樣:
要使上面程式碼例子工作,你自己不需編寫任何SQL程式碼。Orcas中提供的LINQ到SQL物件關係映射器會處理獲取,跟蹤,和更新映射到你的資料庫資料定義和預存程式的物件。你只要使用任何LINQ擴充方法對結果進行過濾和構形即可,LINQ到SQL會執行獲取資料所需的SQL程式碼(注意,上面的 Average和Max 擴充方法很明顯地不會從資料表中傳回所有的資料行,它們會使用TSQL的聚合函數來計算資料庫中的值,然後只傳回一個標量值)。
請觀看我一月份製作的一個錄影,Demo了LINQ到SQL如何顯著地改進了Orcas中的資料生產力。錄影中,你也可以看到新的LINQ到SQL的所見即所得ORM設計器的實戰示範,以及對資料模型編寫LINQ程式碼時程式碼編輯器提供的完整的 intellisense。
結語
希望上面的文章給了你一個對擴充方法工作原理的基本理解,以及你能夠利用它們來實現的一些酷擴充性方式。跟任何擴充性機制一樣,我要告誡你別一開始就濫建新的擴充方法。不能因為你有一個閃亮的新鎯頭,就意味著世界上所有的東西突然都變成釘子了!
想著手開始嘗試擴充方法的話,我建議你先探究一下Orcas中System.Linq命名空間中提供的標準查詢運算子。這些運算子提供了對任何陣列,集合,XML資料流,或關聯式資料庫做豐富的查詢的支援,可以極大地改進你操作資料時的生產力。我認為你會發現它們極大地減小了你要在你應用中編寫的程式碼量,允許你編寫非常乾淨和描述性強的語句。它們也允許你在你編碼中得到查詢邏輯自動的intellisense 和編譯時檢查。
在下幾個星期裡,我將繼續這個關於Orcas中新語言特性的系列,探討匿名類和類的推斷(Type Inference),還會討論Lambda的細節和其他酷特性。很明顯地,我還會地更多地討論LINQ。
希望本文對你有所幫助,
Scott