文章目錄
本書翻譯目的為個人學習和知識共用,其著作權屬原作者所有,如有侵權,請告知本人,本人將立即對發帖採取處理。
允許轉載,但轉載時請註明本著作權聲明資訊,禁止用於商業用途!
部落格園:韓現龍
Introducing to Microsoft LINQ目錄
不必通過學習LINQ來全面理解C# 3.0語言的增強部分。例如,(LINQ)這個新的語言特性中沒有一項涉及到CLR變更。LINQ需要新的編譯器(C# 3.0 或是 Microsoft Visual Basic 9.0),這些編譯器產生的中間代碼可以很好的在Microsoft .NET 2.0 下運行,給您提供可以使用的LINQ庫。
儘管如此,在這一章裡,我們整理出您需要清楚理解並且在LINQ中要用到的那些C#的特性進行一個簡潔的描述(從C# 1.x 到 C# 3.0)。如果您打算跳過這一章,您也可以在想理解LINQ文法內部究竟是怎麼回事的時候回過頭來查看本章。
重溫C# 2.0
C# 2.0 在很多方面改進了原有的C#語言。例如,泛型(Generics)的使用讓開發人員可以用C#定義方法和類擁有一個或多個型別參數。泛型(Generics)是LINQ的一個支柱。
在本章中,我們將描述一些對LINQ來說很重要的C# 2.0文法:泛型(Generics),匿名方法(Anonymous methods,它們是C# 3.0中lambda運算式的基礎),yield關鍵字以及可以枚舉的介面。若您要對LINQ想要有一個較好的理解,您須對這些概念有一個好的理解。
泛型(Generics)
很多程式設計語言通過定義具體的類型和嚴格的轉換規則來處理變數和對象。用強型別語言寫的代碼缺少泛化的條件。請思考如下代碼:
int Min( int a, int b ) {if (a < b) return a;else return b;}
為了使用這個代碼,我們需要一個包含每種型別參數的不同的Min版本來進行比較。習慣於使用對象做為泛型(Generics)的預設類型的開發人員可能會寫出如下方法:
object Min( object a, object b ) {if (a < b) return a;else return b;}
不幸的是,通用的物件類型之間(<)操作符無法使用。我們需要使用一個通用的介面(Interface)來處理:
IComparable Min( IComparable a, IComparable b ) {if (a.CompareTo( b ) < 0) return a;else return b;}
然而,即便解決了這個問題,我們將面對一個更大的問題:這個Min方法不確定的結果類型。調用Min可以把IComparable類型轉換成int類型,但是可能會導致異常且一定會提高CPU的使用成本。
int a = 5, b = 10;int c = (int) Min( a, b );
C# 2.0 是用泛型(Generics)來解決這個問題的。泛型(Generics)的基本原理就是把類型轉換從C#編譯器處理轉移到CLR的Jitter進行處理。下面是Min方法的泛型版:
T Min<T>( T a, T b ) where T : IComparable<T> {if (a.CompareTo( b ) < 0) return a;else return b;}
【小貼士】
Jitter是.Net運行時的Just-In-Time 編譯部分。它把IL代碼翻譯成機器碼。當您編譯.Net代碼時,編譯器把它們成生IL代碼,然後在第一次處理前,通過Jitter把IL代碼編譯成機器碼。
把類型處理轉到Jitter進行是個不錯方法:Jitter可以產生同一段代碼的多種版本,其中總有一種可以使用。這個辦法類似一個宏的擴充,不同之處在於,這種最佳化是用來避免代碼擴散,即泛型方法(Generics Method)的所有版本都使用參考類型作為泛型(Generics)類型且共用相同的編譯好的代碼,僅僅是調用的方法不同。
用泛型(Generics)的方法來寫如下的代碼:
int a = 5, b = 10;
int c = (int) Min( a, b );
您可以寫出如下代碼:
int a = 5, b = 10;
int c = Min<int>( a, b );
之前的問題沒了,代碼運行比以前更快。並且,編譯器可以從參數推斷使用T類型泛型(Generics)的Min方法,所以我們可以寫如下的代碼:
int a = 5, b = 10;
int c = Min( a, b );
類型推斷(Type Inference)
類型介面(Type Inference)是一個關鍵特性。它允許你寫出更多的抽象代碼,寫這些抽象的代碼讓編譯器處理關於類型的細節。然而,C#的類型轉換機制在編譯時間不能保證類型都正確,也不能攔截錯誤碼(例如,調用完全不相容的類型的時候)。
泛型(Generics)不僅可以定義泛型方法(Generics Method),而且和類以及介面一樣可以使用型別宣告。正如前面所說,這本書的目的不是詳細地解釋泛型(Generics),而是想提醒您泛型(Generics)和LINQ的結合將會用著非常舒服。
委託(Delegates)
委託(Delegate)是封裝了一個或多個方法的類。在其內部,一個代理儲存了一些方法的指標列表,每個指標都對應於一個含有執行個體方法的類。
一個委託(Delegate)可以包含若干個方法,但是本章我們只討論包含一個方法的委託(Delegate)。抽象點看,這個委託(Delegate)類型象一個代碼容器。容器中的代碼是不可更改的,但是它可以獨立的被棧調用或是儲存一個變數。它儲存一個執行個體對象,這樣就可以延長對象的生命週期直到委託被有效使用。
委託(Delegate)的文法演化是匿名方法(Anonymous Method)的基礎,這部分內容我們下一章會提到。聲明一個委託(Delegate)其實是定義一個可以執行個體化本身的類型。委託(Delegate)聲明需要一個完整的方法簽名。在Listing 2-1中,我們聲明了3種不同的類型:有著相同方法簽名的它們中的每一種都只能通過外部的方法進行執行個體化。
Listing 2-1: Delegate declaration
delegate void SimpleDelegate();
delegate int ReturnValueDelegate();
delegate void TwoParamsDelegate( string name, int age );
委託(Delegate)是以前C函數指標的升級安全版。使用C# 1.x,委託(Delegate)可以僅通過明確的建立對象來產生,如Listing 2-2所示:
Listing 2-2: Delegate instantiation (C# 1.x)
public class DemoDelegate {
void MethodA() { … }
int MethodB() { … }
void MethodC( string x, int y ) { … }
void CreateInstance() {
SimpleDelegate a = new SimpleDelegate( MethodA );
ReturnValueDelegate b = new ReturnValueDelegate ( MethodB );
TwoParamsDelegate c = new TwoParamsDelegate( MethodC );
// …
}
}
使用最原始的文法來建立委託(Delegate)是十分乏味的:即使當前的類型強於需要被委託的類型,您仍然不得不去瞭解委託類(Delegate Class)的名字,因為不允許使用別的任何類型。這樣,委託(Delegate)類型才能安全的使用。.
C# 2.0考慮到這點,並允許您跳過那部分文法。之前我們看到的委託執行個體不使用新的關鍵字也可以建立。您只需要知道方法名即可。編譯器會推斷出委託的類型。如果您指定了一個可變的簡單委託(SimpleDelegate)類型,那麼C#編譯器會自動的產生新的簡單委託(SimpleDelegate)代碼,這一點適用於任何委託(Delegate)類型。Listing 2-3中展示的C# 2.0版和C# 1.x版的代碼會產生相同的IL代碼。
Listing 2-3: Delegate instantiation (C# 2.0)
public class DemoDelegate {
void MethodA() { … }
int MethodB() { … }
void MethodC( string x, int y ) { … }
void CreateInstance() {
SimpleDelegate a = MethodA;
ReturnValueDelegate b = MethodB;
TwoParamsDelegate c = MethodC;
// …
}
// …
}
您也可以自訂一個泛型的委託類型,顯然在一個泛型類(Generic Class)中定義一個委託對於LINQ來說是一個很重要很有用的的功能。
委託的一個常用點是把一些代碼注入到一個現存的方法中。在Listing 2-4中,我們假定存在一個我們不想改變的名叫Repeat10Times的方法。
Listing 2-4: Common use for a delegate
public class Writer {
public string Text;
public int Counter;
public void Dump() {
Console.WriteLine( Text );
Counter++;
}
}
public class DemoDelegate {
void Repeat10Times( SimpleDelegate someWork ) {
for (int i = 0; i < 10; i++) someWork();
}
void Run1() {
Writer writer = new Writer();
writer.Text = "C# chapter";
this.Repeat10Times( writer.Dump );
Console.WriteLine( writer.Counter );
}
// …
}
當前用的回呼函數被定義為SimpleDelegate,我們想通過注入一個字串統計這個方法被調用了多少次。定義一個Writer類,把執行個體化的資料作為Dump方法的參數。正如您看見的,我們需要定義一個單獨的類,只用來存放需要用的代碼和資料。一條捷徑是採用類似的方式但是使用匿名方法(Anonymous Method)。
匿名方法(Anonymous Methods)
在前面的章節中,我們提到了委託的共用。C# 2.0中有一種代碼寫法,即在Listing 2-4 中言簡意賅的使用匿名方法(Anonymous Method)。如Listing 2-5 例子所示。
Listing 2-5: Using an anonymous method
public class DemoDelegate {
void Repeat10Times( SimpleDelegate someWork ) {
for (int i = 0; i < 10; i++) someWork();
}
void Run2() {
int counter = 0;
this.Repeat10Times( delegate {
Console.WriteLine( "C# chapter" );
counter++;
} );
Console.WriteLine( counter );
}
// …
}
在代碼中,我們不再聲明Writer類。編譯器自動的為我們聲明一個類並為其命了名。相反,我們定義一個調用Repeat10Times的方法,看上去好像我們把這部分代碼作為一個參數在使用。然而,編譯器把代碼轉換為一種類似有明確類使用的共同委託(Common Delegate)的形式。在代碼中這種轉換的唯一證明就是代碼塊前的關鍵字。這個文法被稱為匿名方法(Anonymous Method)。
【小貼士】
記住你無法將代碼傳入變數中,能傳入的只是一個指標。在您繼續操作前請在確定一遍您沒有弄錯。
在程式碼片段前標明作為委託關鍵字的匿名方法(Anonymous Method)。當我們有一個包含一個或多個參數的委託方法時,這個文法允許我們把參數的名字定義為委託(delegate)。Listing 2-6 中的代碼定義了一個作為TwoParamsDelegate委託(delegate)類型的匿名方法。
Listing 2-6: Parameters for an anonymous method
public class DemoDelegate {
void Repeat10Times( TwoParamsDelegate callback ) {
for (int i = 0; i < 10; i++) callback( "Linq book", i );
}
void Run3() {
Repeat10Times( delegate( string text, int age ) {
Console.WriteLine( "{0} {1}", text, age );
} );
}
// …
}
我們用2個明確的參數作為委託(delegate)來代替Repeat10Times方法。可以這樣理解:如果您移除text和age兩個參數的聲明,委託(delegate)將產生2個名稱為定義的錯誤。
要點
您將在C# 3.0中(間接地)使用委託和匿名方法,基於這個原因,深刻地理解這些概念是非常重要的。只有這樣,您在面對越來越複雜的情況時能從抽象的高度來把握它。
枚舉(Enumerators) 和 Yield關鍵字
C# 1.x 定義兩個介面來支援枚舉。命名空間System.Collections包含這些聲明,如Listing 2-7所示:
Listing 2-7: IEnumerator and IEnumerable declarations
public interface IEnumerator {bool MoveNext();object Current { get; }void Reset();}public interface IEnumerable {IEnumerator GetEnumerator();}
介面就可以枚舉執行個體化IEnumerator介面的對象。這個枚舉可以調用MoveNext方法直到它返回False。
Listing 2-8中的代碼就是按這種方式定義了一個類。如您所見,CountdownEnumerator這個類更複雜些,且它單獨的實現了枚舉邏輯。在這個例子裡,並不是真的要枚舉什麼,只是簡單的把Countdown類中定義的StartCountdown這個數字開始的降序數字返回出來,Countdown類也是一個枚舉類。
Listing 2-8: Enumerable class
public class Countdown : IEnumerable {public int StartCountdown;public IEnumerator GetEnumerator() {return new CountdownEnumerator( this );}}public class CountdownEnumerator : IEnumerator {private int _counter;private Countdown _countdown;public CountdownEnumerator( Countdown countdown ) {_countdown = countdown;Reset();}public bool MoveNext() {if (_counter > 0) {_counter--;return true;}else {return false;}}public void Reset() {_counter = _countdown.StartCountdown;}public object Current {get {return _counter;}}}
CountdownEnumerator在委託真的發生時才被使用。例如,Listing 2-9中顯示了一種可能的使用方式。
Listing 2-9: Sample enumeration code
public class DemoEnumerator {public static void DemoCountdown() {Countdown countdown = new Countdown();countdown.StartCountdown = 5; IEnumerator i = countdown.GetEnumerator(); while (i.MoveNext()) {int n = (int) i.Current;Console.WriteLine( n );} i.Reset(); while (i.MoveNext()) {int n = (int) i.Current;Console.WriteLine( "{0} BIS", n );}}// …}
調用GetEnumerator就提供了枚舉對象。我們用兩個迴圈來顯示這個重設方法的用法。我們不得不把當前的傳回值轉換為int型是因為我們使用的是非通用枚舉介面。
【小貼士】 C# 2.0中介紹了支援泛型的枚舉。命名空間System.Collections.Generic包含了IEnumerable<T>和IEnumerator<T>的聲明。這些介面無需轉換即可把資料轉成object類型。對於枚舉實值型別來說,這種能力顯得非常重要,因為這樣可能不會有裝箱或是拆箱操作帶來的影響。
自從 C# 1.x起,枚舉可以方便地使用foreach來進行操作。Listing 2-10中代碼所示的結果和之前例子相同。
Listing 2-10: Enumeration using a foreach statement
public class DemoEnumeration {public static void DemoCountdownForeach() {Countdown countdown = new Countdown();countdown.StartCountdown = 5; foreach (int n in countdown) {Console.WriteLine( n );} foreach (int n in countdown) {Console.WriteLine( "{0} BIS", n );}}// …}
使用foreach,編譯器初始化GetEnumerator後,在每一個迴圈之前調用MoveNext。真正的不同在於,每次迴圈都產生了代碼且沒有調用重設方法:即產生了2個CountdownEnumerator對象而不是1個對象。
【小貼士】 foreach也可以用於那些不帶枚舉介面(IEnumerable interface)但是有一個公用枚舉方法(GetEnumerator method)的類。
C# 2.0通過編譯器自動產生一個從枚舉介面(IEnumerator interface)繼承且返回枚舉方法(GetEnumerator method)的類來說明yield。Yield僅可以用在return或是break這兩個關鍵字之前。Listing 2-11代碼中產生了一個相當於前面介紹的CountdownEnumerator的類。
Listing 2-11: Enumeration using a yield statement
public class CountdownYield : IEnumerable {public int StartCountdown;public IEnumerator GetEnumerator() {for (int i = StartCountdown - 1; i >= 0; i--) { yield return i;}}}
從邏輯上看,yield return等價於在下一個MoveNext被調用前暫緩執行(suspending execution)。回憶一下,GetEnumerator方法在整個枚舉中只被調用一次,它繼承自IEnumerator介面且返回一個類。那個類從包含yield的方法中繼承了那些聲明。
一個包含了yield的方法稱為一個迭代器(iterator):迭代器(iterator)可以包含多個yield。Listing 2-12中的代碼絕對有效且在功能上等效於之前的StartCountdown值為5的CountdownYield類。
Listing 2-12: Multiple yield statements
public class CountdownYieldMultiple : IEnumerable {public IEnumerator GetEnumerator() { yield return 4; yield return 3; yield return 2; yield return 1; yield return 0;}}
通過使用IEnumerator泛型,可以定義一種CountdownYield強型別,如Listing 2-13所示。
Listing 2-13: Enumeration using yield (typed)
public class CountdownYieldTypeSafe : IEnumerable<int> {public int StartCountdown; IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } public IEnumerator<int> GetEnumerator() {for (int i = StartCountdown - 1; i >= 0; i--) {yield return i;}}}
這個強型別版本中包含兩個GetEnumerator方法:一個是和非泛型代碼相容的(返回IEnumerable),另一個是強型別(返回IEnumerator<int>)。
LINQ廣泛的使用枚舉(Enumeration)和yield。即使它們被封裝,當你調試代碼時也應當想到它們。
譯者:部落格園 Temptation
校稿:部落格園 韓現龍
上一篇:微軟免費圖書《Introducing Microsoft LINQ》翻譯-Chapter1.5 and Chapter1.6:LINQ的現狀及前景
下一篇:C#3.0特性