認識枚舉

8.4  簡易不簡單:認識枚舉

本節將介紹以下內容:

— 枚舉類型全解

— 位標記應用

— 枚舉應用規則

8.4.1  引言

在哪裏可以看到枚舉?打開每個文件的屬性,我們會看到只讀、隱藏的選項;操作一個文件時,你可以採用只讀、可寫、追加等模式;設置系統級別時,你可能會選擇緊急、普通和不緊急來定義。這些各式各樣的信息中,一個共同的特點是信息的狀態分類相對穩定,在.NET中可以選擇以類的靜態字段來表達這種簡單的分類結構,但是更明智的選擇顯然是:枚舉。

事實上,在.NET中有大量的枚舉來表達這種簡單而穩定的結構,FCL中對文件屬性的定義爲System.IO.FileAttributes枚舉,對字體風格的定義爲System.Drawing.FontStyle枚舉,對文化類型定義爲System.Globlization.CultureType枚舉。除了良好的可讀性、易於維護、強類型的優點之外,性能的考慮也佔了一席之地。

關於枚舉,在本節會給出詳細而全面的理解,認識枚舉,從一點一滴開始。

8.4.2  枚舉類型解析

1.類型本質

所有枚舉類型都隱式而且只能隱式地繼承自System.Enum類型,System.Enum類型是繼承自System.ValueType類型唯一不爲值類型的引用類型。該類型的定義爲:

public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible

從該定義中,我們可以得出以下結論:

l  System.Enum類型是引用類型,並且是一個抽象類。

l  System.Enum類型繼承自System.ValueType類型,而ValueType類型是一切值類型的根類,但是顯然System.Enum並非值類型,這是ValueType唯一的特例。

l  System.Enum類型實現了IComparable、IFormattable和IConvertible接口,因此枚舉類型可以與這三個接口實現類型轉換。

.NET之所以在ValueType之下實現一個Enum類型,主要是實現對枚舉類型公共成員與公共方法的抽象,任何枚舉類型都自動繼承了Enum中實現的方法。關於枚舉類型與Enum類型的關係,可以表述爲:枚舉類型是值類型,分配於線程的堆棧上,自動繼承於Enum類型,但是本身不能被繼承;Enum類型是引用類型,分配於託管堆上,Enum類型本身不是枚舉類型,但是提供了操作枚舉類型的共用方法。

下面我們根據一個枚舉的定義和操作來分析其IL,以從中獲取關於枚舉的更多認識:

enum LogLevel

{

    Trace,

    Debug,

    Information,

    Warnning,

    Error,

    Fatal

}

將上述枚舉定義用Reflector工具翻譯爲IL代碼,對應爲:

.class private auto ansi sealed LogLevel

    extends [mscorlib]System.Enum

{

    .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Debug = int32(1)

    .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Error = int32(4)

    .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Fatal = int32(5)

    .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Information = int32(2)

    .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Trace = int32(0)

    .field public specialname rtspecialname int32 value__

    .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Warnning = int32(3)

}

從上述IL代碼中,LogLevel枚舉類型的確繼承自System.Enum類型,並且編譯器自動爲各個成員映射一個常數值,默認從0開始,逐個加1。因此,在本質上枚舉就是一個常數集合,各個成員常量相當於類的靜態字段。

然後,我們對該枚舉類型進行簡單的操作,以瞭解其運行時信息,例如:

public static void Main()

{

    LogLevel logger = LogLevel.Information;

    Console.WriteLine("The log level is {0}.", logger);

}

該過程實例化了一個枚舉變量,並將它輸出到控制檯,對應的IL爲:

.method public hidebysig static void Main() cil managed

{

    .entrypoint

    .maxstack 2

    .locals init (

        [0] valuetype InsideDotNet.Framework.EnumEx.LogLevel logger)

    L_0000: nop

    L_0001: ldc.i4.2

    L_0002: stloc.0

    L_0003: ldstr "The log level is {0}."

    L_0008: ldloc.0

    L_0009: box InsideDotNet.Framework.EnumEx.LogLevel

    L_000e: call void [mscorlib]System.Console::WriteLine(string, object)

    L_0013: nop

    L_0014: ret

}

分析IL可知,首先將2賦值給logger,然後執行裝箱操作(L_0009),再調用WriteLine方法將結果輸出到控制檯。

2.枚舉規則

討論了枚舉的本質,我們再回過頭來,看看枚舉類型的定義及其規則,例如下面的枚舉定義略有不同:

enum Week: int

{

    Sun = 7,

    Mon = 1,

    Tue,

    Wed,

    Thur,

    Fri,

    Sat,

    Weekend = Sun

}

根據以上定義,我們瞭解關於枚舉的種種規則,這些規則是定義枚舉和操作枚舉的基本綱領,主要包括:

l  枚舉定義時可以聲明其基礎類型,例如本例Week枚舉的基礎類型指明爲int型,默認情況時即爲int。通過指定類型限定了枚舉成員的取值範圍,而被指定爲枚舉聲明類型的只能是除char外的8種整數類型:byte、sbyte、short、ushort、int、uint、long和ulong,聲明其他的類型將導致編譯錯誤,例如Int16、Int64。

l  枚舉成員是枚舉類型的命名常量,任意兩個枚舉常量不能具有同樣的名稱符號,但是可以具有相同的關聯值。

l  枚舉成員會顯式或者隱式與整數值相關聯,默認情況下,第一個元素對應的隱式值爲0,然後各個成員依次遞增1。還可以通過顯式強制指定,例如Sun爲7,Mon爲1,而Tue則爲2,並且成員Weekend和Sun則關聯了相同的枚舉值。

l  枚舉成員可以自由引用其他成員的設定值,但是一定注意避免循環定義,否則將引發編譯錯誤,例如:

enum MusicType

{

    Blue,

    Jazz = Pop,

    Pop

}

編譯器將無法確知成員Jazz和Pop的設定值到底爲多少。

l  枚舉是一種特殊的值類型,不能定義任何的屬性、方法和事件,枚舉類型的屬性、方法和事件都繼承自System.Enum類型。

l  枚舉類型是值類型,可以直接通過賦值進行實例化,例如:

Week myweek = Week.Mon;

也可以以new關鍵字來實例化,例如:

Week myweek = new Week();

值得注意的是,此時myweek並不等於Week枚舉類型中定義的第一個成員的Sun的關聯值7,而是等效於字面值爲0的成員項。如果枚舉成員不存在0值常數,則myweek將默認設定爲0,可以從下面代碼來驗證這一規則:

enum WithZero

{

    First = 1,

    Zero = 0

}

enum WithNonZero

{

    First = 1,

    Second

}

class EnumMethod

{

    public static void Main()

    {

        WithZero wz = new WithZero();

        Console.WriteLine(wz.ToString("G"));

        WithNonZero wnz = new WithNonZero();

        Console.WriteLine(wnz.ToString("G"));

    }

}

//執行結果

//Zero

//0

因此,以new關鍵字來實例化枚舉類型,並非好的選擇,通常情況下我們應該避免這種操作方式。

l  枚舉可以進行自增自減操作,例如:

Week day = (Week)3;

day++;

Console.WriteLine(day.ToString());

通過自增運算,上述代碼輸出結果將爲:Fri。

8.4.3  枚舉種種

1.類型轉換

(1)與整型轉換

因爲枚舉類型本質上是整數類型的集合,因此可以與整數類型進行相互的類型轉換,但是這種轉換必須是顯式的。

//枚舉轉換爲整數

int i = (int)Week.Sun;

//將整數轉換爲枚舉

Week day = (Week)3;

另外,Enum還實現了Parse方法來間接完成整數類型向枚舉類型的轉換,例如:

//或使用Parse方法進行轉換

Week day = (Week)Enum.Parse(typeof(Week), "2");

(2)與字符串的映射

枚舉與String類型的轉換,其實是枚舉成員與字符串表達式的相互映射,這種映射主要通過Enum類型的兩個方法來完成:

l  ToString實例方法,將枚舉類型映射爲字符串表達形式。可以通過指定格式化標誌來輸出枚舉成員的特定格式,例如“G”表示返回普通格式、“X”表示返回16進制格式,而本例中的“D”則表示返回十進制格式。

l  Parse靜態方法,將整數或者符號名稱字符串轉換爲等效的枚舉類型,轉換不成功則拋出ArgumentException異常,例如:

Week myday = (Week)Enum.Parse(typeof(Week), "Mon", true);

Console.WriteLine(myday);

因此,Parse之前最好應用IsDefined方法進行有效性判斷。對於關聯相同整數值的枚舉成員,Parse方法將返回第一個關聯的枚舉類型,例如:

Week theDay = (Week)Enum.Parse(typeof(Week), "7");

Console.WriteLine(theDay.ToString());

//執行結果

//Sun

(3)不同枚舉的相互轉換

不同的枚舉類型之間可以進行相互轉換,這種轉換的基礎是枚舉成員本質爲整數類型的集合,因此其過程相當於將一種枚舉轉換爲值,然後再將該值映射到另一枚舉的成員。

MusicType mtToday = MusicType.Jazz;

Week today = (Week)mtToday;

(4)與其它引用類型轉換

除了可以顯式的與8種整數類型進行轉換之外,枚舉類型是典型的值類型,可以向上轉換爲父級類和實現的接口類型,而這種轉換實質發生了裝箱操作。小結枚舉可裝箱的類型主要包括:System.Object、System.ValueType、System.Enum、System.IComparable、System.IFormattable和System.IConvertible。例如:

IConvertible iConvert = (IConvertible)MusicType.Jazz;

Int32 x = iConvert.ToInt32(CultureInfo.CurrentCulture);

Console.WriteLine(x);

1.常用方法

System.Enum類型爲枚舉類型提供了幾個值得研究的方法,這些方法是操作和使用枚舉的利器,由於System.Enum是抽象類,Enum方法大都是靜態方法,在此僅舉幾個簡單的例子點到爲止。

以GetNames和GetValues方法分別獲取枚舉中符號名稱數組和所有符號的數組,例如:

//由GetName獲取枚舉常數名稱的數組

foreach (string item in Enum.GetNames(typeof(Week)))

{

    Console.WriteLine(item.ToString());

}

//由GetValues獲取枚舉常數值的數組

foreach (Week item in Enum.GetValues(typeof(Week)))

{

    Console.WriteLine("{0} : {1}", item.ToString("D"), item.ToString());

}

應用GetValues方法或GetNames方法,可以很容易將枚舉類型與數據顯式控件綁定來顯式枚舉成員,例如:

ListBox lb = new ListBox();

lb.DataSource = Enum.GetValues(typeof(Week));

this.Controls.Add(lb);

以IsDefined方法來判斷符號或者整數存在於枚舉中,以防止在類型轉換時的越界情況出現。

if(Enum.IsDefined(typeof(Week), "Fri"))

{

    Console.WriteLine("Today is {0}.", Week.Fri.ToString("G"));

}

以GetUnderlyingType靜態方法,返回枚舉實例的聲明類型,例如:

Console.WriteLine(Enum.GetUnderlyingType(typeof(Week)));

8.4.4  位枚舉

位標記集合是一種由組合出現的元素形成的列表,通常設計爲以“位或”運算組合新值;枚舉類型則通常表達一種語義相對獨立的數值集合。而以枚舉類型來實現位標記集合是最爲完美的組合,簡稱爲位枚舉。在.NET中,需要對枚舉常量進行位運算時,通常以System.FlagsAttribute特性來標記枚舉類型,例如:

[Flags]

enum ColorStyle

{

    None = 0x00,

    Red = 0x01,

    Orange = 0x02,

    Yellow = 0x04,

    Greeen = 0x08,

    Blue = 0x10,

    Indigotic = 0x20,

    Purple = 0x40,

    All = Red | Orange | Yellow | Greeen | Blue | Indigotic | Purple

}

FlagsAttribute特性的作用是將枚舉成員處理爲位標記,而不是孤立的常數,例如:

public static void Main()

{

    ColorStyle mycs = ColorStyle.Red | ColorStyle.Yellow | ColorStyle.Blue;

    Console.WriteLine(mycs.ToString());

}

在上例中,mycs實例的對應數值爲21(十六進制0x15),而覆寫的ToString方法在ColorStyle枚舉中找不到對應的符號。而FlagsAttribute特性的作用是將枚舉常數看成一組位標記來操作,從而影響ToString、Parse和Format方法的執行行爲。在ColorStyle定義中0x15顯然由0x01、0x04和0x10組合而成,示例的結果將返回:Red, Yellow, Blue,而非21,原因正在於此。

位枚舉首先是一個枚舉類型,因此具有一般枚舉類型應有的所有特性和方法,例如繼承於Enum類型,實現了ToString、Parse、GetValues等方法。但是由於位枚舉的特殊性質,因此應用於某些方法時,應該留意其處理方式的不同之處。這些區別主要包括:

l  Enum.IsDefined方法不能應對位枚舉成員,正如前文所言位枚舉區別與普通枚舉的重要表現是:位枚舉不具備排他性,成員之間可以通過位運算進行組合。而IsDefined方法只能應對已定義的成員判斷,而無法處理組合而成的位枚舉,因此結果將總是返回false。例如:

Enum.IsDefined(typeof(ColorStyle), 0x15)

Enum.IsDefined(typeof(ColorStyle), "Red, Yellow, Blue")

MSDN中給出瞭解決位枚舉成員是否定義的判斷方法:就是將該數值與枚舉成員進行“位與”運算,結果不爲0則表示該變量中包含該枚舉成員,例如:

if ((mycs & ColorStyle.Red) != 0)

    Console.WriteLine(ColorStyle.Red + " is in ColorStyle");

l  Flags特性影響ToString、Parse和Format方法的執行過程和結果。

l  如果不使用FlagsAttribute特性來標記位枚舉,也可以在ToString方法中傳入“F”格式來獲得同樣的結果,以“D”、“G”等標記來格式化處理,也能獲得相應的輸出格式。

l  在位枚舉中,應該顯式的爲每個枚舉成員賦予有效的數值,並且以2的冪次方爲單位定義枚舉常量,這樣能保證實現枚舉常量的各個標誌不會重疊。當然你也可以指定其它的整數值,但是應該注意指定0值作爲成員常數值時,“位與”運算將總是返回false。

8.4.5  規則與意義

l  枚舉類型使代碼更具可讀性,理解清晰,易於維護。在Visual Stuido 2008等編譯工具中,良好的智能感知爲我們進行程序設計提供了更方便的代碼機制。同時,如果枚舉符號和對應的整數值發生變化,只需修改枚舉定義即可,而不必在漫長的代碼中進行修改。

l  枚舉類型是強類型的,從而保證了系統安全性。而以類的靜態字段實現的類似替代模型,不具有枚舉的簡單性和類型安全性。例如:

public static void Main()

{

    LogLevel log = LogLevel.Information;

    GetCurrentLog(log);

}

private static void GetCurrentLog(LogLevel level)

{

    Console.WriteLine(level.ToString());

}

試圖爲GetCurrentLog方法傳遞整數或者其他類型參數將導致編譯錯誤,枚舉類型保證了類型的安全性。

l  枚舉類型的默認值爲0,因此,通常給枚舉成員包含0值是有意義的,以避免0值遊離於預定義集合,導致枚舉變量保持非預定義值是沒有意義的。另外,位枚舉中與0值成員進行“位與”運算將永遠返回false,因此不能將0值枚舉成員作爲“位與”運算的測試標誌。

l  枚舉的聲明類型,必須是基於編譯器的基元類型,而不能是對應的FCL類型,否則將導致編譯錯誤。

8.4.6  結論

枚舉類型在BCL中佔有一席之地,說明了.NET框架對枚舉類型的應用是廣泛的。本節力圖從枚舉的各個方面建立對枚舉的全面認知,通過枚舉定義、枚舉方法和枚舉應用幾個角度來闡釋一個看似簡單的概念,對枚舉的理解與探索更進了一步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章