marshine
(原文排版格式:http://www.marshine.com)
reversion:2004/5/28
修改說明:感謝Ninputer提到的CLS相容問題,同時修改了原來版本沒有提及的Equals改寫,以及修改"=="重載的不完善代碼,和增加enum struct內容
reversion:2004/6/4
增加kirc提到的Enum的Flags特性,因為文本超長,新的版本可以在http://www.marshine.com上閱讀。
常量類型的表示
系統中常常有一些屬性的屬性值是固定的一組值,它們的範圍是封閉的(有限數量),比如國家代碼(每個國傢具有唯一的代碼,而在一定時期國家的數量是確定的)、性別類型(男、女)。在現代 程式語言中,一種典型的表示方式是枚舉類型(Enum)。Enum表示封閉範圍的類型,常常由程式語言作為一種資料類型直接支援,例如C,C#等。C#支援的enum在C的基礎上提供了型別安全的能力,下面是用C#定義的性別枚舉類型:
public enum Sex {
Male,
Female,
}
Java不支援enum資料類型,Java認為C提供的enum並不是型別安全的,通常使用稱之為Typesafe Enum Class的設計模式來獲得類似的效果(參見[Joshua01] P80,Item21 :Replace enum constructs with classes)。Enum Class不允許外部構造執行個體成員(建構函式為private),提供靜態類型成員執行個體來表示封閉範圍。使用Enum Class方式來表示Sex類型可定義如下(C#):
public class Sex{
// 私人構造保證範圍的封閉性
private Sex() {
}
pubic static readonly Sex Male = new Sex():
pubic static readonly Sex Female = new Sex():
}
同enum一樣,可以使用Sex.Male或Sex.Female的方式來訪問常量屬性,與靜態常量欄位不一樣(如靜態字串、整數),enum和Enum Class可以提供強型別的compile time檢查以及提供更好的資料封裝性和代碼可讀性。例如使用常量類型設定和比較屬性值:
// 設定屬性值
Sex sex = Sex.Male;
// 比較
if (sex == Sex.Male) {
// ... ...
}
如果Sex是使用Enum定義的,則上面比較的實際上是Enum欄位的值;如果Sex是使用Enum Class定義的,則比較的是靜態執行個體成員的引用地址,當然也可以使用Equals方法來比較。
雖然Enum Class是來自於Java的設計模式,但在C#中並非沒有意義,因為Enum Class提供了比Enum類型更強大的能力。
Enum與Enum Class的比較
Enum與Enum Class均提供了封裝常量的能力,都能夠實現編譯時間的強型別檢查,使用封閉範圍防止非法值。不過,因為實現機制的不同,這兩種方式也具有不同的特點。
Enum在C#中是一種實值型別(Value Type),其基底類型必須是整數類型(如Int16),因此Enum也具有實值型別所具有的優點——比參考型別(Reference Type)更高的效率,定義簡單。但是其缺點不能實現自訂的行為,無法提供常量更多的屬性。
Enum Class就沒有這種限制,雖然Enum Class本身並不設計為可以繼承,但可以修改基類(System.Object)的行為以提供更加豐富的能力(如修改ToString方法,根據使用者的本地語言輸出本地化的國家名稱),也可以提供更多的屬性 。例如我們提供一個候選的國家列表,除了能顯示國家名稱外,可以提供國家代碼、語言代碼資訊。
Enum Class的問題
但Enum Class也有它的缺點,上面的設計中Enum Class通過進程內靜態成員引用地址相同來進行比較,但是當將一個序列化後的Enum Class執行個體還原序列化後,CLR會建立一個新的執行個體,從而造成還原序列化值不等於序列化前值的現象:
IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
MemoryStream stream = new MemoryStream();
// 序列化Sex.Male的值
formatter.Serialize(stream, Sex.Male);
stream.Seek(0,SeekOrigin.Begin);
// 還原序列化
Sex sex = (Sex)formatter.Deserialize(stream);
Console.WriteLine(sex == Sex.Male);
上面的代碼將輸出false。因此通過引用的方式是有局限性的,在Java中這是一個比較棘手的問題,需要修改還原序列化的行為(參看[Joshua01]P171)。C#與Java的實現機制不一樣,無法通過修改還原序列化的行為來返回同一個常量執行個體, 但C#提供了操作符重載的能力,我們可以通過重載操作符“==”來解決這個問題,同時為了保持CLS相容以及與Equals的行為一致,還需要改寫Equals方法:
[Serializable]
public class Sex{
// 性別類型名
private string sexName;
// 私人構造保證範圍的封閉性
private Sex(string sexName) {
this.sexName = sexName;
}
public static readonly Sex Male = new Sex("Male");
public static readonly Sex Female = new Sex("Female");
// 提供重載的"=="操作符,使用sexName來判斷是否是相同的Sex類型
public static bool operator ==(Sex op1, Sex op2) {
if (Object.Equals(op1, null)) return Object.Equals(op2, null);
return op1.Equals(op2);
}
public static bool operator !=(Sex op1,Sex op2) {
return !(op1 == op2);
}
public override bool Equals(object obj) {
Sex sex = obj as Sex;
if (obj == null) return false;
return sexName == sex.sexName;
}
public override int GetHashCode() {
return sexName.GetHashCode ();
}
}
通過操作符重載,不再使用引用地址來比較常量,而是通過值比較(如上面的sexName),因此要求每個常量執行個體必須具有唯一的標識值。 在不支援操作符重載的語言中,不能使用"=="來比較兩個常量值是否相等,而應該使用Equals方法來代替。
Enum Class的設計
Enum Class一般符合下列規則:
- 私人建構函式,保證外部無法建立類執行個體(同時也使得類無法繼承)。
- 靜態唯讀執行個體欄位表示常量。
- 重載操作符"==",保證序列化後的值也能比較相等。當需要在進程間傳遞(如分布式應用)或需要序列化時,必須實現"=="操作符的重載。
- 改寫Equals方法,保持"=="行為和Equals一致。(改寫Equals一般也同時改寫GetHashCode方法 )
除此之外,還通常改寫ToString方法以提供顯示友好的名字,因為Java和.Net都在綁定或顯示對象時使用ToString方法(Java中為toString方法)輸出作為預設的對象顯示字串,比如將Sex數組綁定到ListBox或者使用Console.Write輸出時。下面的代碼改寫ToString方法以提供友好顯示的輸出:
public class Sex{
... ...
public override string ToString() {
return sexName;
}
}
當然我們也可以利用ToString提供本地化支援,返回本地語言的字串。
Enum Class另外一種常見的職責是提供不同值系統之間的類型轉換,如當從資料庫中讀取值時,利用Parse方法將資料庫中值轉換為對象系統的常量執行個體,而在儲存時提供方法轉換為資料庫的實值型別:
public class Sex{
... ...
// 根據一個符合指定格式的字串傳回型別執行個體。
public static Sex Parse(string sexName){
switch (sexName) {
case "Male" : return Male;
... ...
}
}
// 返回資料存放區的值。
public string ToDBValue(){
return sexName;
}
}
使用Enum還是Enum Class?
根據Enum和Enum Class的特點,我們可以根據對常量類型的要求決定使用Enum還是Enum Class。
以下情境適合使用Enum:
- 常量類型用於內部表示,不用於顯示名字。
- 常量值不需要提供附加的屬性。例如只需要知道國家代碼,而不需要獲得國家的其它屬性
Enum Class可以適用於更多的情境:
- 常用於可提供友好資訊的類型。如本地化支援的類型名顯示,或者顯示與枚舉名不一致的名字,例如Country.CHN可顯示為"China"。
- 提供更多的常量屬性。
- 提供更加豐富的行為。如Parse方法。
- 對常量進行分組。如Country.Asia包含亞洲國家。
使用Struct來表示枚舉
如果範圍不封閉,但希望提供一些常量,也可以使用struct,如System.Drawing.Color結構中的系統預設顏色設定。採用struct來設計enum值同Enum Class方式沒有本質的差異,只是struct必須提供無參數建構函式,因此無法實現封閉範圍。
參考:
[Joshua01]
Effective Java Programming Language Guide , Joshua Bloch, Pearson Education,2001.
Java 高效編程指南(中文版),機械工業出版社,2002