19.C#2.0介紹 C#2.0引入了幾項語言擴展,其中最重要的是泛型、匿名方法、迭代器和不完整類型(partial type)。
泛型可以讓類、結構、接口、委托和方法,通過他們所存儲和操縱的數據的類型被參數化。泛型是很有用的,因為他們提供了更強的編譯時類型檢查,減少了數據類型之間的顯式轉換,以及裝箱操作和運行時類型檢查。 匿名方法可以讓代碼塊以內聯的方式潛入到期望委托值的地方。匿名方法與Lisp 編程語言中的λ函數(lambda function)相似。C#2.0支持“closures”的創建,在其中匿名方法可以訪問相關局部變量和參數。 迭代器是可以遞增計算和產生值的方法。迭代器讓類型指定foreach語句如何迭代它的所有元素,變得很容易。 不完整類型可以讓類、結構和接口被拆分成多個部分存儲在不同的源文件中,這更利于開發和維護。此外,不完整類型允許某些類型的機器生成的部分與用戶編寫的部分之間的分離,因此增加由工具產生的代碼很容易。
本章將介紹這些新特征。介紹完之后,接下來的四章提供了這些特征的完整的技術規范。
C#2.0的語言擴展主要被設計用于確保與現存的代碼之間最大的兼容性。例如,盡管C#2.0對于where、yield 和partial這些詞在特定上下文中賦予了特別的意義,但這些詞仍然可被用作標識符。實際上,C# 2.0沒有增加任何可能與現有代碼中的標識符沖突的關鍵字。
19.1 泛型 泛型可以讓類、結構、接口、委托和方法,通過他們所存儲和操縱的數據的類型被參數化。C#泛型對于使用Eiffel或Ada的泛型的用戶,或者對于C++模板的用戶來說是很熟悉的;但他們將不用再去忍受后者的眾多的復雜性。
19.1.1為什么使用泛型 沒有泛型的話,通用目的的數據結構可以采用object類型存儲任何類型的數據。例如,下面的Stack類在一個object數組中存儲數據,而它的兩個方法,Push和Pop相應地使用object接收和返回數據。
public class Stack
{
object[] items;
int count;
public void Push(object item){…}
public object Pop(){…}
}
盡管使用類型object可以使得Stack類更加靈活,但這樣做也并不是沒有缺點。例如,你可以將一個任何類型的值,諸如,Customer的一個實例壓入(Push)堆棧。但當你取回一個值時,Pop方法的結果必須被顯式地強制轉換到合適的類型,為一個運行時類型檢查去編寫代碼,以及帶來的性能不利影響,是很令人討厭的。
Stack stack = new Stack();
Stack.Push(new Customer());
Customer c = (Customer)stack.Pop();
如果一個值類型的值,例如一個int被傳遞到Push方法,它將會被自動裝箱。當后面獲得這個int 時,它必須使用一個顯式的強制轉換而被取消裝箱。
Stack stack = new Stack();
Stack.Push(3);
int I = (int)stack.Pop();
這種裝箱和取消裝操作增加了性能開銷,因為它們涉及到動態內存的分配和運行時類型檢查。
Stack類的更大的問題是,它不能強制放置在堆棧上的數據種類。實際上,Customer實例可以被壓入堆棧,而取回它時可能被強制轉換到錯誤的類型。
Stack stack = new Stack();
Stack.Push(new Customer());
String s = (string)stack.Pop();
盡管先前的代碼是Stack類的一種不恰當用法,但這段代碼從技術上說是正確的,并且也不會報告編譯時錯誤。問題直到代碼執行時才會冒出來,在這一點上將會拋出一個InvalidCastException異常。
如果Stack類具有能夠指定其元素的類型能力,那么很顯然它能從這種能力得到好處。使用泛型,這將會變成可能。
19.1.2 創建和使用泛型 泛型為創建具有類型參數(type parameter)的類型提供了工具。下面的例子聲明了一個帶有類型參數T的泛型Stack類。類型參數在類名字之后的“<“和“>”分界符中指定。這里沒有object與別的類型之間的相互轉換,Stack<T>的實例接受它們被創建時的類型,并且存儲那個類型的數據而沒有轉換它。類型參數T充當一個占位符,直到使用的時候才指定一個實際的類型。注意,T被用作內部items數組的元素類型、Push方法參數的類型和Pop方法的返回值類型。
Public class Stack<T>
{
T[] items;
int count;
public void Push(T item){…}
public T Pop(){…}
}
當泛型類Stack<T>被使用時,T所代替的實際類型將被指定。在下面的例子中,int 將被作為T的類型參數而給出。
Stack<int> stack = new Stack<int>();
Stack.Push(3);
int x = stack.Pop();
Stack<int>類型被稱為構造類型(constructed type)。在Stack<int>類型中,T的每次出現都被使用類型參數int代替。當Stack<int>的實例被創建時,items數組的本地存儲就是一個int[]而不是object[],與非泛型Stack相比,它提供了更高的存儲效率。同樣地,在int值上的Stack<int>操作的Push和Pop方法,將會使得壓入其他類型的值到堆棧中出現一個編譯時錯誤,并且當取回值的時候也不需要轉換回它們原始的類型。
泛型提供了強類型,意義例如壓入一個int到Customer對象堆棧將會出現錯誤。就好像Stack<int>被限制只能在int值上操作,同樣Stack<Customer>也被限制用于Customer對象。
對于下面的例子,編譯器將會在最后兩行報告錯誤。
Stack<Customer> stack = new Stack<Customer>();
Stack.Push(new Customer());
Customer c = stack.Pop();
stack.Push(3); //類型不匹配錯誤
int x = stack.Pop(); //類型不匹配錯誤
泛型類型聲明可以有任意數量的類型參數。先前的Stack<T>例子 只有一個類型參數,但一個通用的Dictionary類可能有兩個類型參數,一個用于鍵(key)的類型,另一個用于值(value)的類型。
public class Dictionary<K , V>
{
public void Add(K key , V value){…}
public V this[K key]{…}
}
當Dictionary<K , V> 被使用時,必須提供兩個類型參數。
Dictionary<string , Customer> dict = new Dictionary<string , Customer>();
Dict.Add(“Peter”, new Customer());
Custeomer c = dict[“Perter”];
19.1.3泛型類型實例化 與非泛型類型相似,被編譯過的泛型類型也是由中間語言[Intermediate Language(IL)]指令和元數據表示。泛型類型的表示當然也對類型參數的存在和使用進行了編碼。
當應用程序首次創建一個構造泛型類型的實例時,例如,Stack<int>,.NET公共語言運行時的實時編譯器(JIT)將在進程中把泛型IL和元數據轉換為本地代碼,并且將類型參數替換為實際的類型。對于那個構造泛型類型的后續引用將會使用相同的本機代碼。從一個泛型類型創建一個特定構造類型的過程,稱為泛型類型實例化(generic type instantiation)。
.NET公共語言運行時使用值類型為每個泛型類型實例創建了一個本地代碼的特定拷貝,但對于所有的引用類型它將共享那份本地代碼的單一拷貝(因為,在本地代碼級別,引用只是帶有相同表示的指針)。
19.1.4約束 一般來講,泛型類不限于只是根據類型參數存儲值。泛型類經常可能在給定類型參數的類型的對象上調用方法。例如,Dictionary<K , V>類中的Add方法可能需要使用CompareTo方法比較鍵值。
public class Dictionary<K , V>
{
public void Add(K key , V value)
{
…
if(key.CompareTo(x)<0){…}//錯誤,沒有CompareTo方法
…
}
}
因為為K所指定的類型參數可能是任何類型,可以假定key參數存在的唯一成員,就是那些被聲明為object類型的,例如,Equals,GetHashCode和ToString;因此,在先前例子中將會出現編譯時錯誤。當然,你可以將key參數強制轉換到一個包含CompareTo方法的類型。例如,key參數可能被強制轉換到IComparable接口。
public class Dictionary<K , V>
{
public void Add(K key , V value)
{
…
if(((IComparable)key).CompareTo(x)<0){…}
…
}
}
盡管這種解決辦法有效,但它需要在運行時的動態類型檢查,這也增加了開銷。更糟糕的是,它將錯誤報告推遲到了運行時,如果鍵(key)沒有實現IComparable接口將會拋出InvalidCastException異常。
為了提供更強的編譯時類型檢查,并減少類型強制轉換,C#允許為每個類型參數提供一個約束(constraint)的可選的列表。類型參數約束指定了類型必須履行的一種需求,其目的是為了為類型參數被用作實參(argument)。約束使用單詞where聲明,隨后是類型參數的名字,接著是類或接口類型的列表,和可選的構造函數約束new()。
public class Dictionary<K, V> where K :IComparable
{
public void Add(K key , V value)
{
…
if(key.CompareTo(x)<0){…}
…
}
}
給定這個聲明,編譯器將會確保K的任何類型實參是實現了IComparable接口的類型。
并且,在調用CompareTo方法之前也不再需要對key參數進行顯式地強制轉換。為類型參數作為一個約束而給出的類型的所有成員,對于類型參數類型的值時直接有效的。
對于一個給定的類型參數,你可以指定任意數量的接口作為約束,但只能有一個類。每個約束的類型參數有一個單獨的where 語句。在下面的例子中,類型參數K有兩個接口約束,類型參數e有一個類約束和一個構造函數約束。
public class EntityTable<K, E>
where K:IComparable<K>,IPersisable
where E:Entity, new()
{
public void Add(K key , E entity)
{
…
if(key.CompareTo(x)<0){…}
…
}
}
在前面的例子中,構造函數約束new(),確保為E用作類型參數的類型具有一個公有的、無參數構造函數,并且它允許泛型類使用new E()創建該類型的實例。
類型參數約束應該很小心的使用。盡管它們提供了更強的編譯時類型檢查,在某些情況下增強了性能,但它們也限制了泛型類型的可能的用法。例如,泛型類List<T>可能約束T實現IComparable接口,由此它的Sort方法將可以比較項的大小。然而,這么做卻使得沒有實現IComparable 接口的類型不能使用List<T>,即使是在這些情形下,Sort方法根本就沒有
|