C# Idioms:Enum還是Enum Class(枚舉類) )

來源:互聯網
上載者:User

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類型更強大的能力。

EnumEnum 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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.