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框架對枚舉類型的應用是廣泛的。本節力圖從枚舉的各個方面建立對枚舉的全面認知,通過枚舉定義、枚舉方法和枚舉應用幾個角度來闡釋一個看似簡單的概念,對枚舉的理解與探索更進了一步。