C# Idioms: Enum還是Enum Class(枚舉類)
2024-07-21 02:19:53
供稿:網友
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