Professional Documents
Culture Documents
本章涵蓋的範圍
泛型型別與方法
.NET 2.0 的泛型集合
泛型的限制
和其他語言做比較
1
在開始之前先講一個小故事 :有一天我和我太太到雜貨店去買些東西,在離開之
前她問我是否有帶清單,確認之後我們就離開了雜貨店,在這我們犯了一個錯誤,
就是我太太問我是否有帶「購物清單」,但我以為是另外一張清單,在這邊就提到
C# 2.0 一個新特性:泛型(因為清單可以是代表各種類型的清單),之後透過購物
清單去購買一些商品(購買的動作就會使用到匿名方法),如果可以透過這樣的方
式來達成,不管是哪一種清單,都會自動的去採購對應的商品,那這就是 C# 2.0
泛型的主要目的。泛型的其中一個用途,就是可以讓不同型別去做相同動作的事情。
1
這邊的目的是為了方便介紹本章的主題。
64 y Part II:C# 2.0 改善的議題
下面會先讓各位了解目前所遇到的問題,然後會說明泛型是如何做改善。
3.1 為什麼需要泛型?
在寫 C# 1.0 程式的時候,不知道各位是否常常有使用到轉型?如果希望讓任何型
別的資料都可以放到集合中,可以發現到一件事情,就是程式碼要做大量的轉型工
作。然而這些集合物件所有的 API,參數以及回傳值都是使用 object 的型別,雖然
object 可以用來接收不同型別的物件,但是 object 所能提供的資訊是相當少的,所
以為了能夠取得到原來型別的物件,就只能透過轉型來達成。
轉型是否就是不好呢?如果程式碼不常使用到轉型,其實轉型並不算壞,但是相反
的,大量的轉型將會影響到效能上的運作,轉型是為了要告訴編譯器更多的資訊,
而編譯器也會信任我們所做的型別轉換,所以到執行時期時,才會對型別做檢查,
並判斷是否屬於有效的轉型。
為了避免不必要的轉型,因此就有了一些新的想法,例如宣告變數或是方法時,可
以透過額外的資訊,來告訴目前屬於哪種型別,可以方便其他人了解在集合內所要
處理的資料型別。泛型可以確保在編譯時期間,判斷傳入參數的型別是相符的(過
去就需要手動的去檢查,或是在執行時期發生錯誤時,才知道該如何修正)。有了
這些額外的資訊,可以讓程式碼更加的有效率,並提供編譯器檢查型別的方式;而
Visual Studio 的 IntelliSense 也可以取得額外的資訊,會判斷集合內可以加入哪些
型 別 ( 例 如 宣 告 List<string> , Visual Studio 就 會 知 道 下 次 要 加 入 的 資 料 需 為
string);在方法的呼叫上,可以清楚的定義要傳入的型別以及回傳型別,並且程
式碼可以在一開始,就定義好使用的型別,這樣可以也可以更容易閱讀。
Chapter 3 偉大的泛型 y 65
不管在執行效能上的改善,或是型別安全的檢查,這些都讓泛型變得更有價值,這
邊會說明兩件事情,第一,對於編譯器來說,需要花更多的時間來做型別的檢查,
但相對著,它可以減少執行時期所花費的時間;第二,JIT 可以更聰明的操作數值
型別,不需要再透過 boxing 以及 unboxing 的方式,來處理數值型別與參考型別的
轉換,然而這些的改變,可以大量的提升執行效能,並且減少記憶體消耗。
關於泛型的優點,可能和靜態語言非常相似(比起動態語言來說),例如在編譯時
期,可以對型別進行檢查、提供程式碼更多的資訊、IDE 支援 IntelliSense,以及較
好的執行效能,這些的優點是相當容易理解的,例如我們在使用一般的 API(像是
ArrayList),它無法區分型別的差異,因為都使用 object 型別來存取資料,並且需
要對元素進行轉型,若透過泛型就不是這樣的做法了。
接下來就真正的開始來使用泛型。
3.2 使用簡單的泛型
泛型可以說是一個相當大的主題,因為包含了許多重要的概念,假如想要了解泛型
的每一個功能以及特性,可以去參考 C# 2.0 的規格書,裡面會針對不同的案例進
行詳細的說明,當然不需要去了解一些偏僻的主題(事實上,對於其他的特性也是
一樣,例如我們不需要了解關於所有變數的指派,或是型別的轉換,只需要能夠修
正好程式碼,讓編譯器可以正常的編譯)。
66 y Part II:C# 2.0 改善的議題
在這一節當中,會討論到一些平常使用的泛型,包含了一些泛型的 API,以及自行
建立的泛型方法。在這邊先提醒各位,如果在閱讀本章時,有遇到一些不了解的概
念,會建議各位針對想要了解的部分來閱讀,例如如何使用 .NET Framework 所提
供的泛型型別以及方法,從這些感興趣的議題開始學起,會比較容易了解泛型的用
法,而不建議在看不懂的情況下繼續閱讀本章。
3.2.1 透過範例學習:泛型的資料字典
使用泛型型別(generic type),可以容易找出錯誤的地方,並且在閱讀程式碼的
時候,可以容易的推測程式碼的運作方式,並不需要因為型別的轉換,而花費許多
時間(泛型的其中一項優點,在於編譯時期會檢查型別的安全,所以對於開發人員
來說,只需要確認程式碼是否能夠編譯成功),然而本章的目的在於說明泛型的好
處,所以各位不需再對 C# 1.0 的型別錯誤而感到困擾。
建立新的對照表,並用來計
算單字出現的次數
將一段文字切分成
單字的陣列
加入或更新對照表
Chapter 3 偉大的泛型 y 67
從對照表印出每
個索引鍵(key)
與值(value)
在 CountWords 方法中,可以用來計算單字出現的次數。在一開始建立了一個空的
Dictionary 的泛型物件n,並設定對照表的索引鍵為 string 的型別,而值為 int 型別;
接著使用了常規表示式o,並將段落文字分割成多個單字,這邊是依據空白字元來
當做分割條件;之後透過迴圈來判斷單字是否已經在對照表當中,假如存在,就會
將目前單字的個數加 1,若不存在,則會初始一個新的單字,並設定成 1p。不知
道各位有沒有發現到,在取得以及存入到 Dictionary 的泛型物件時,並沒有做任何
的轉型動作,這些存取的動作,也都是使用索引子(indexer)來達成,像是在
frequencies[word]++; 這 行 程 式 碼 , 也 許 某 些 人 可 能 就 會 發 現 , 也 可 以 使 用
frequencies[word] = frequencies[word]+1; 的寫法。
3.2.2 泛型型別與型別參數
泛型會透過兩種方式來呈現:泛型型別(generic type, 包含了類別、介面、委派還
有結構,但列舉並不支援)以及 泛型方法 (generic method),這兩個不同的地方
在於:泛型型別是用來指定類別要以哪種型別來處理資料,例如上面提到的
Dictionary<string, int>,就會將類別內有用到 TKey 以及 TValue 的型別,取代成
string 以及 int;然而泛型方法,只會在方法內進行型別的取代,而用來設定這些型
別的稱為型別參數(type parameter)。
68 y Part II:C# 2.0 改善的議題
泛型的型別參數(type parameter,以下統稱為型別參數),它會代表一個真實的
型別,而型別參數會是使用角括號 <> 來宣告,若有多個型別參數需要定義,則會
使 用 逗 號 隔 開 , 所 以 Dictionary<TKey, TValue> 的 型 別 參 數 就 是 TKey 以 及
TValue,當我們使用這些泛型的時候,就會透過此參數來指定真實的型別,例如
在範例 3.1 將 TKey 指定成 string 型別,TValue 指定成 int 型別。
在這邊會提到很多關於泛型的專有名詞,這是為了讓本章的主題能夠更清楚
的表達,當然這也是能讓我們在溝通上能夠一致,對於要熟悉 C# 語言規格
書來說,這是相當有幫助的。 WARNING
Dictionary<TKey,TValue>
Dictionary<TKey,TValue>
(非約束泛型型別)
(unbound generic type)
Specification of
型別參數
type arguments
特別化
Dictionary<string,int>
Dictionary<string,int> Dictionary<byte,long>
Dictionary<byte,long> (etc)
其他
Hashtable (建構式型別)
(constructed type) (建構式型別)
(constructed type)
使用 new 來實體化
Instantiation 使用 new 來實體化
Instantiation 使用 new 來實體化
Instantiation
Instance of Dictionary<string,
Instance of int> Dictionary<byte,long>
Instance of
Hashtable 的實體
Dictionary<string,int> Dictionary<byte,long>
的實體
非泛型的概觀
Nongeneric 泛型概觀
Generic
blueprints blueprints
圖 3.1 非約束泛型扮演著建構式型別的輪廓,並且也扮演著實體物件的框架。
Chapter 3 偉大的泛型 y 69
2
關於建構式型別,若深入的探討還可以再分成兩種類型 ,分別為開放型別(open
type)與封閉型別(close type),這兩者的差別在於:是否能夠指定其他地方的型
別參數。開放型別允許我們對繼承的介面或是類別,透過本身的型別參數去指定父
類別的型別參數;封閉型別則是不會指定繼承類別的型別參數(沒有繼承的泛型型
別也算是封閉型別),例如 SubClass<T, S> 繼承自 Dictionary<TKey, TValue>,寫
法為 public class SubClass<T, S> : Dictionary<T, string>{…},對於 SubClass 就是
一種開放型別,因為本身的型別參數,有指定到父類別的型別參數;若 SubClass<T,
S> 繼承自 Dictionary<string, int> 或是 ArrayList,則此時就是屬於封閉型別,因為
本身的型別參數沒有使用在父類別上。在 3.4.4 節會討論更多關於非約束型別的議
題,以及如何搭配 typeof 運算子。
關於泛型的型別參數,會在我們指定型別時,將類別內有用到型別參數宣告的變數
取代成指定的型別,3.1 顯示了 Dictionary<TKey, TValue> 非約束泛型型別的一些
方法以及屬性,這是還沒對型別參數做任何設定,而右邊跟它對照的
Dictionary<string, int>,則我們已經建立成的建構式型別。
表 3.1 關於泛型型別的進入點,以及對型別參數做設定
泛型型別的方法 透過型別參數取代後的方法
public void Add public void Add
(TKey key, TValue value) (string key, int value)
public TValue this [TKey key] public int this [string key]
{ get; set; } { get; set; }
public bool ContainsValue public bool ContainsValue
(TValue value) (int value)
public bool ContainsKey public bool ContainsKey
(TKey key) (string key)
在表 3.1 有一件很重要的事要說明,上面並沒有任何的泛型方法,都只是泛型型別
所提供的方法,這些方法上的型別(TKey 或 TValue),會在宣告泛型型別時決定。
宣告泛型類別
2
對於開放型別與封閉型別或許可能還不是很了解,不必太過於執著這些名詞上的差別,在這邊會比較
希望能夠讓各位明白泛型的使用方式以及一些限制。
70 y Part II:C# 2.0 改善的議題
實作泛型介面
宣告無參數的建
構子 宣告的方法有使用
型別參數
在 上 面 的 程 式 碼 當 中 , 可 以 看 到 Dictionary<TKey, TValue> 實 作 了
IEnumerable<KeyValuePair<TKey, TValue>> 的泛型介面,並且 KeyValuePair 型別
參數的來源,會是從 Dictionary 的 TKey 與 TValue 取得,若根據前面的範例初始
成 Dictionary<string, int>,此時介面會取代成 IEnumerable<KeyValuePair<string,
int>>,對於 IEnumerable<T> 來說,我們將型別參數 T 取代成 KeyValuePair<TKey,
TValue> 的結構,這種我們稱為 雙重泛型 (doubly generic)的介面,由於實作了
IEnumerable<T> 介面,所以在範例 3.1 才可以使用 foreach 來取得 Dictionary<string,
int> 的索引鍵與值。這邊可以注意到一件事情,在建構子的部分,並沒有定義型別
參數的角括號,這是因為型別參數是屬於型別而非建構子,所以我們會將角括號放
在型別上。
型 別 參 數 像 方 法 一 樣 , 可 以 具 有 多 載 性 ( overloading ) , 所 以 就 可 以 定 義 像
MyType、MyType<T>、MyType<T, U>、MyType<T, U , V>或是其他更多的。通
常型別參數的命名,並沒有很多人會去注意它,但是對於使用的人來說,可能會難
以了解這些型別參數的目的,例如泛型的方法,它是允許存在兩個相同的方法,但
型別參數的個數需不同,這對於開發人員可能就會感到相當的困擾,畢竟有兩個相
同的方法,卻有不同個數的型別參數。
Chapter 3 偉大的泛型 y 71
上面介紹了關於泛型型別的使用及概念,接下來將探討泛型方法的使用。
3.2.3 泛型方法與泛型的宣告
在上面也提到了泛型型別,當指定了泛型的型別參數時,會自動將方法的參數,全
部取代成指定的型別,然而泛型方法的概念,可能會比泛型型別來得難懂,這是因
為大多數的人對於方法有一定的了解,會很直覺的認為方法是具有回傳型別以及參
數,而泛型方法則是更進一步的擴充,允許每個方法擁有各自的型別參數。
方法名稱
Method name 參數的型別(泛型委派)
Parameter type (generic delegate)
圖 3.2 分析泛型方法
3
這邊將 ConvertAll 方法的參數名稱做修改,將原本的 converter 改成 conv,至於其他部分則是和文件定
義的相同。
72 y Part II:C# 2.0 改善的議題
所以取代後的方法,由左至右的說明將會是如下:
方法會回傳一個 List<Guid>。
方法名稱為 ConvertAll。
方法只有一個型別參數(TOutput),並且將型別參數指定成 Guid。
此方法只有一個參數,參數的名稱為 conv,並且它是屬於 Converter<string,
Guid>的泛型型別。
建立 List 的集合物件,並且存放
的元素為 int 型別
建立委派實體
呼叫泛型方法,用來
轉換 List 物件內的所
有元素
範例 3.3 實作泛型方法(在非泛型的類別上)
74 y Part II:C# 2.0 改善的議題
說到這邊,相信對泛型也有了簡單的了解,如果還不太了解關於泛型的概念,例如
非約束型別、建構式型別、封閉型別或是開放型別等,可以再回去閱讀一下,接下
來會探討比較複雜的泛型,讓各位可以更深入的了解以及使用。
當你在實際撰寫泛型的時候,相信會慢慢的體會到泛型的用意,尤其在自定一個泛
型型別或是泛型方法,將會發現泛型可以減少許多程式碼的重寫,這是因為執行的
邏輯都相同,但卻要針對不同型別做處理,就像上面提到的 ConvertAll 方法,然
而泛型也可以讓型別變得更安全,也可以避免轉型上的錯誤,這些是強型別所帶來
的好處。
3.3 泛型的進階
接下來會繼續介紹泛型的其他特性。首先來談談型別的條件約束(type constraint, 在
後面會簡稱條件約束),條件約束是用來限制型別參數,可以用來限制要繼承哪個
類別或介面,若指定的型別不符合條件約束,此時就會被拒絕使用,然而這樣的功
能,對於在建立泛型型別或是泛型方法是相當有用的,至少可以將型別參數限制在
一定的範圍內。
之後將會提到型別推論(type inference)。在使用泛型方法時,不需要每次都明確
的指定型別參數,編譯器會自動的偵測泛型方法,並且推論所使用的型別參數,讓
Chapter 3 偉大的泛型 y 75
程式碼可以看起來更簡單,也可以確保型別上的安全,這部分會在第三篇關於 C#
編譯器的章節討論。
在本節的最後一部分,會介紹型別參數要如何設定預設值,以及型別參數是如何進
行比對的動作,之後會透過一個完整的範例來說明泛型的特性,讓我們可以對這些
特性更加的了解。
雖然本節介紹的有點深入,但是這些泛型的特性並沒有想像中的難,有很多的特性
會幫助我們在程式的開發上,接下來就先從條件約束開始介紹。
3.3.1 型別的條件約束
到目前所介紹的泛型,在型別參數都是沒有任何的限制,例如使用到的 List<int> 或
是 Dictionary<object, FileMode>,全部都是無條件約束,不過對於集合物件的使
用,通常不會去在意元素的類型資料,不過在某些情況下,可能會希望型別參數是
有限制的,例如型別參數只能是屬於參考型別或是數值型別,換句話說,我們會希
望可以針對型別參數來指定有效的型別,在 C# 2.0 可以使用條件約束來達到這樣
的需求。
參考型別的條件約束
參考型別的條件約束(reference type constraints),它會在泛型的後面加上 where T :
class,並且型別參數所指定的型別,必須是屬於參考型別,所以型別可以是類別、
介面、陣列或是委派等,下面則是結構使用條件約束的宣告方式:
有效的型別會是如下的列表:
RefSample<IDisposable>
RefSample<string>
RefSample<int[]>
以下是無效的型別:
RefSample<Guid>
RefSample<int>
若將型別參數限制成參考型別時,這時候透過型別參數所宣告的變數(包含方法的
參數),可以透過 == 或是 != 的運算子(也包含 null)來比對。不過這邊會建議各
位,盡量不要使用 == 和 != 運算子,因為這些運算子只會比對參考的位址,而不
會比對兩個值是否相等,即使這些型別有對運算子多載(operator overload),例
如 string 型別有對 == 運算子重新多載,它仍然會比對兩個物件的參考值。例如附
錄的補充程式碼:<補充程式碼,參見附錄 B 譯註 3>
數值型別的條件約束
數值型別的條件約束(value type constraints),只允許型別參數必須為數值型別(會
使用 where T : struct 來表示數值型別的限制),這當中也包含了 Nullable 型別(將
會在第 4 章描述),所以條件約束的宣告方式如下:
有效的型別:
ValSample<int>
ValSample<FileMode>
無效的型別:
ValSample<object>
ValSample<StringBuilder>
雖然很少會使用到參考型別或是數值型別的條件約束,除非是在下一將會介紹的
Nullable 型別,就有可能會使用到數值型別的條件約束,但是接下來的兩個條件約
束,會是平常寫程式容易使用到的。
建構子的條件約束
建構子的條件約束(constructor type constraints, 會是使用 where T : new(),若與其
他條件約束一起使用時,一定要將其指定為最後一個),它可以透過型別參數來建
立物件,不過型別必須要有一個無參數的建構子,如果沒有無參數的建構子,則會
被條件約束給限制住。然而建構子的條件約束,可以是參考型別或是數值型別,只
Chapter 3 偉大的泛型 y 77
要是符合非靜態類別、非抽象類別,並且具有公開的無參數建構子(parameterless
constructor),以上這些都是有效的。
下面是關於建構子的條件約束:
方法會回傳我們所指定型別的實體,並且型別必須擁有無參數的建構子,例如使用
CreateInstance<int>(); 或是 CreateInstance<object>(); 都是有效的,但是如果使用
CreateInstance<string>() ; 則會出現錯誤的訊息,這是因為 string 並沒有提供任何無
參數的建構子。
在上面的範例中,透過建構子的條件約束,可以方便我們建立一個實體物件(這是
因為使用無參數的建構子,所以不需要考慮參數初始化的問題),這樣就像是設計
模式(design pattern)所用到的工廠樣版一樣,可以隨時建立需要的實體物件(就
像工廠一樣,需要一直生產產品,但通常都會依照標準的介面來實作),接下來則
是最後一個條件約束。
衍生型別的條件約束
衍生型別的條件約束(derivation type constraints),會限制型別參數必須是衍生自
4
某個類別或是介面 ,這種條件約束是相當的有用,當我們確定型別參數都是繼承
自某個類別時,就可以透過這種條件約束來限制,例如 public class MyGeneric<T>
where T : Stream{…},此時在 MyGeneric 類別內,只要是使用 T 所宣告的變數,
就可以使用 Stream 所提供的方法以及屬性。然而也可以指定某一個型別參數,當
成是另一個型別參數的條件限制,像是 public class SampleClass<T, S> where T : S,
這種我們稱為 Naked 的條件約束(naked constraint)。在表 3.2 當中,顯示一些衍
生型別的條件約束範例,在表格的右半部,則會說明合法與不合法的宣告方式。
4
若型別可以透過隱含的方式來轉換,這樣也符合衍生型別的條件約束,例如有個條件約束為 where T :
IList<Shape>,此時使用 Circle[] 也會是有效的,即使 Circle[] 並沒有實作任何的 IList<Shape>,這邊
就只是隱含的參考轉換而已。
78 y Part II:C# 2.0 改善的議題
表 3.2 衍生型別條件約束的範例
條件約束的宣告 範例
class Sample<T> 合法:Sample<Stream>
where T : Stream Sample<MemoryStream>
不合法:Sample<object>
Sample<string>
struct Sample<T> 合法:Sample<IDisposable>
where T : IDisposable Sample<DataTable>
不合法:Sample<StringBuilder>
class Sample<T> 合法:Sample<string>
where T : IComparable<T> 不合法:Sample<FileInfo>
class Sample<T,U> 合法:Sample<Stream,IDisposable>
where T : U Sample<string,string>
不合法:Sample<string,IDisposable>
但這樣的寫法是不允許的:
條件約束的合併
在前面已經有提過,條件約束是可以同時多個存在。在前面已經有看過條件約束的
寫法,但是還沒有看過將多個條件約束合併在一起,可以很清楚的知道一件事,並
沒有任何的型別同時是屬於參考型別以及數值型別,所以對於這樣的合併是拒絕
的。不同的型別參數,可以擁有獨立的條件約束,並且要使用各自的 where 來限制
型別。下面就來看一下關於一些有效以及無效的合併條件限制:
有效:
無效:
上面是關於條件約束合併的列表,可以簡單的區分別出有效以及無效的版本,只要
記住每個型別參數都需要有各自的 where 來限制。然而第三個合法的條件限制是有
趣的,如果 U 是屬於數值型別並且又繼承自 T,但是 T 是屬於參考型別,這樣不
會覺得不合理嗎?答案是 T 必須是 object 或者是 U 所實作的介面,當然這樣的寫
法會容易讓人混淆,所以不太建議使用這樣的方式。
關於所有的泛型型別的宣告以及條件約束的用法,相信大家已經對這些已經有了一
些認知,接下來會討論型別參數的推論,這和之前提到的 var 關鍵字很像,編譯器
會去推論它真實的型別,然而在前面範例 3.2 的 List.ConvertAll 就是其中的一種,
關於編譯器是如何推算出可能的型別,這就是下一節要說明的主題。
3.3.2 泛型方法的型別推論
在呼叫某一些泛型方法時,可能對於型別參數的指定是多餘的,通常可以從傳入的
的參數來得知是屬於哪一種的型別,這樣對於編譯器來說,就可以知道型別參數所
指定的型別,所以在 C# 2.0 的編譯器,可以讓我們更簡單的來呼叫泛型方法,而
不需要使用角括號來指明它的型別。
上面的寫法看起來簡短許多,而且不需要去指定型別參數的型別,但是在某些情況
下,這可能會造成閱讀上變得更加困難,因為會不知道目前所使用的型別為何,雖
然對於編譯器是會相當的容易識別,不過建議各位可以根據當時的情況來決定,如
果可以很容易看出型別參數的型別,就可以不需要標明。
不過在哪種情況下編譯器會發生錯誤呢?我們來看一個範例,如果將上面的型別參
數指定成 object,並且傳入的參數為字串,對於這樣的寫法仍然是有效,例如下面
的寫法:
List<object> list = MakeList<object> ("Line 1", "Line 2");
在這邊並沒有列出所有的規則(並不推薦去了解關於編譯器的型別推論,除非很有
興趣的想要知道),假如會認為編譯器可能會推論所有的型別參數,並試著去呼叫
沒有指定型別參數的方法(當然也有可能會推論錯誤),這只不過是讓編譯器花較
多的時間去處理這樣的事情,對於開發人員只是少了一個指定型別參數的動作。
Chapter 3 偉大的泛型 y 81
3.3.3 泛型的實作
當你在實作泛型的一些類別或是方法時,大多會使用 T 來當作型別(也就是型別
參數)的名稱,並且在泛型的類別也都使用這樣的命名方式,不過下面有幾點關於
泛型的事情是必須知道的。
使用預設值
現在已經知道在泛型使用的型別,但是要如何取得型別的預設值呢?以及如何對型
別 T 做初始化的動作呢?因為現在不知道型別參數所指定的型別,會無法對型別
設定預設值,並且也不知道它是屬於參考型別或是數值型別,所以就不知道該設定
成 null 或者是 0 的數值。
一般而言,很少會對型別參數設定預設值,但是如果能夠這樣做的話,在某些情況
下是相當有用的,然而 Dictionary<TKey, TValue> 就是一個相當好的例子。在
Dictionary 類別中有提供一個 TryGetValue 的方法,這方法就像數值型別使用的
TryParse 方法一樣,會使用到 out 的參數來取得到數值,並且回傳一個布林值來判
斷是否取值成功,若有取到值,則會將數值寫入到 out 的參數上,但如果失敗的話,
這時候就會取得 TValue 型別的預設值(關於 out 參數的使用,需要將一個變數傳
入到方法中,並且要在參數的前面加上 out 關鍵字,假設目前是 Dictionary<string,
int>,所以就可以這樣呼叫 TryGetValue(“35”, out returnValue);)。
範例 3.4 在泛型上使用預設值
82 y Part II:C# 2.0 改善的議題
在上面的範例,使用了三種不同型別來呼叫泛型方法,分別是 string、int 以及
DateTime 型別,然而在 CompareToDefault 的方法,限制了型別參數必須要實作
IComparable<T> 的介面,這樣就可以呼叫 CompareTo(T)的方法,並傳入一個數
值;在 CompareToDefault 方法當中,會將傳入的值和型別的預設值做比較,例如
在第一個使用 CompareToDefault(“x”),這時會回傳 1(代表傳入的值大於預設值
null),因為 string 參考型別的預設值會是 null,所以傳入字串 “x” 一定會比 null
來的大;而接下來的三行則是會和 int 的預設值做比較,然而 int 的預設值會是 0,
這三行會依序印出 1、0、-1;最後的 DataTime 的預設值將會是 DataTime.MinValue。
直接比較
雖然在範例 3.4 的比較方式是可行的,但總不能每次要比對型別的時候,都限制型
別要實作 IComparable<T> 或是 IEquatable<T> 的介面(會提供強型別的 Equal(T)
方法),但是這些介面,並沒有提供實際型別的額外資訊,然而透過這些介面,就
只能去進行比對的對作,就像呼叫 Equal(object)方法一樣,如果想要比對一個數值
型別的資料,它就會將此數值進行 boxing 的動作,但卻不知道它原本的型別為何。
如果型別參數是無任何條件約束的時候,就可以使用 == 或是 != 運算子來進行比
對,但是這僅限於去判斷變數的值是否為 null,並無法去比對兩個 T 的型別參數。
在某些情況下,型別參數有可能是指派成數值型別(除了 Nullable 型別),若數值
型別和 null 進行比對,將永遠不會相等;當型別參數為參考型別時,對於比對會
是有用的;但是如果是屬於 Nullable 型別的話,在比對的過程中,可能會無法將
Nullable 型別視為 null 值。
當型別參數限制成數值型別時,== 以及 != 運算子將會無法使用。相反的,若型別
參數限制成參考型別時,這種比對的方式,將會取決於型別是否還有額外的條件約
束存在,如果沒有,對於 == 以及 != 的運算子將是可以用來比較的;如果所限制
的型別具有運算子多載(operator overload),例如將運算子 == 以及 != 多載,對
於型別參數的比較,並不會去使用多載的運算子來比較。範例 3.5 使用了參考型別
當作條件約束,而且型別參數指定成 string 的型別。
Chapter 3 偉大的泛型 y 83
範例 3.5 使用 == 以及 != 運算子來比較參考型別
參考的比較
使用 string 運算子
多載來比較
對於兩個類別的比較,比較常使用 EqualityComparer<T> 以及
Comparer<T>,這兩個都是在 System.Collections.Generic 的命
名空間中,並且都實作了 IEqualityComparer<T>(用來比對
Dictionary 的 key 是相當有用的)以及 IComparer<T> 的介面(對
於排序是相當的有用),然而 Default 的屬性,會回傳一個預
設的比較物件(如果型別參數指定為 string,就會回傳 string
型別預設的 StringComparer 比較物件),關於更多的可以參考 MSDN 的文件。下
面將會有範例介紹 EqualityComparer<T> 的使用。
完整的範例:使用一對數值來表達
接下來要介紹的這個範例,會實作一個泛型的介面,並且型別參數會指定成
Pair<TFirst, TSecond>,這是用來記錄兩個數值,有點類似 key/value 的概念,但要
注意的是,這兩個數值彼此之間是沒有任何的相關性,在程式中也提供了一些屬
性,並覆寫(override)Equal 以及 GetHashCode 的方法,允許我們的型別可以做
相等的比對,範例 3.6 顯示了完整的程式碼。
84 y Part II:C# 2.0 改善的議題
範例 3.6 使用泛型類別來比較兩個值
在上面的程式碼,會將透過建構子傳入數值並存放在變數上,然而可以透過唯讀的
屬性取出,在這邊我們有實作了 IEquatable<Pair<TFirst, TSecond>>,並使用強型
別的方式。在這類別中覆寫了 Equal 以及 GetHashCode 的方法,這是為了透過 Equal
方法來比對兩個 Pair 是否相同,並且在我們所寫的 Equal 方法中,使用到了