C# Idioms:Enum還是Enum Class(枚舉類)
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