1.問題
//Code #01
class Tester
{
static void Main()
{
Alignment a = new Alignment();
Console.WriteLine(a.ToString("D"));
Alignment b = Alignment.Left;
Console.WriteLine(b.ToString("D"));
}
}
假定Left是Alignment枚舉的第一個成員,你認為這兩種初始化枚舉變量的方式是否等效?如果不等效,它們有什麼差別?
2.兩種初始化方法的對比
2.1 第一個枚舉成員的值為0
如果我們沒有為Alignment指定第一個成員的值:
//Code #02
enum Alignment
{
Left,
Center,
Right
}
Code #01的輸出結果將是:
0
0
我們再把Code #01的Main反編譯成IL:
//Code #03
.method private hidebysig static
void Main(string[] args) cil managed
{
.entrypoint
// Code Size: 50 byte(s)
.maxstack 2
.locals (
CsWritingLab.Alignment alignment1,
CsWritingLab.Alignment alignment2)
L_0000: nop
L_0001: ldc.i4.0
L_0002: stloc.0
L_0003: ldloc.0
L_0004: box CsWritingLab.Alignment
L_0009: ldstr "D"
L_000e: call instance string [mscorlib]System.Enum:string)
L_0013: call void [mscorlib]System.Console::WriteLine(string)
L_0018: nop
L_0019: ldc.i4.0
L_001a: stloc.1
L_001b: ldloc.1
L_001c: box CsWritingLab.Alignment
L_0021: ldstr "D"
L_0026: call instance string [mscorlib]System.Enum:string)
L_002b: call void [mscorlib]System.Console::WriteLine(string)
L_0030: nop
L_0031: ret
}
從上面的代碼中,我們可以看到這兩種初始化方式是等效的。實質上,下面這4句(在此時)是等效的(它們產生一樣的IL代碼):
Alignment a = new Alignment();
Alignment b = Alignment.Left;
Alignment d = (Alignment)0;
Alignment c = 0;
2.2 第一個枚舉成員的值非0
如果我們手動指定Alignment的第一個成員的值呢?
//Code #04
enum Alignment
{
Left = 1,
Center,
Right
}
Code #01的輸出結果將有點令人疑惑:
0
1
為什麼會這樣呢?讓我們從IL代碼中看看編譯器是如何理解此時的Main的:
// Code #05
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code Size: 50 byte(s)
.maxstack 2
.locals (
CsWritingLab.Alignment alignment1,
CsWritingLab.Alignment alignment2)
L_0000: nop
L_0001: ldc.i4.0
L_0002: stloc.0
L_0003: ldloc.0
L_0004: box CsWritingLab.Alignment
L_0009: ldstr "D"
L_000e: call instance string [mscorlib]System.Enum:string)
L_0013: call void [mscorlib]System.Console::WriteLine(string)
L_0018: nop
L_0019: ldc.i4.1
L_001a: stloc.1
L_001b: ldloc.1
L_001c: box CsWritingLab.Alignment
L_0021: ldstr "D"
L_0026: call instance string [mscorlib]System.Enum:string)
L_002b: call void [mscorlib]System.Console::WriteLine(string)
L_0030: nop
L_0031: ret
}
從上面的IL代碼中,我們可以看出這兩種初始化方式已經不再被理解為一樣的了。對比Code #03和Code #05,你會發現,改變的僅僅是L_0019行:
ldc.i4.0 -> ldc.i4.1
也就是說,使用枚舉的第一個成員來初始化枚舉變量,編譯器懂得根據枚舉的定義來作出相應的調整,從而編譯出符合我們預期的代碼。這種處理方式實際上使用了多態性的思維。
而此時,
Alignment a = new Alignment();
相當於
Alignment a = (Alignment)0;
或者
Alignment a = 0;
3.new、值類型的默認構造函數和值類型的默認值
通常我們使用new來調用引用類型的實例構造函數(Instance Constructors),或者自定義值類型的非默認實例構造函數(Non-Default Instance Constructors)。然而,我們也可以使用new來調用值類型(包括內置簡單類型和自定義類型)的默認構造函數,例如:
int i = new int();
這裡,new調用int的默認構造函數把i初始化為對應的默認值——0。當然,這個默認構造函數由.NET自動提供(但你不能手動提供)。也就是說,使用new來調用值類型的默認構造函數,該值類型將被自動設為對應的默認值。.NET的值類型分為簡單類型(Simple types)、枚舉類型(Enum types)和結構類型(Struct types)。
3.1 簡單類型(Simple types)的默認值
對於簡單類型(Simple types),它們的默認值如下表所示:
Simple Type Default Value bool false byte 0 char '\0' decimal 0.0M double 0.0D float 0.0F int 0 long 0L sbyte 0 short 0 uint 0 ulong 0 ushort 0
3.2 枚舉類型(Enum types)的默認值
對於枚舉類型(Enum types),.NET會自動將字面值0(literal 0)隱式地轉換為對應的枚舉類型。
3.2.1 有一個0值成員
如果枚舉類型中的某個成員被賦予0值(不要求是第一個成員),那麼枚舉變量所儲存的值就是該成員的值。假定Alignment的成員被賦值如下:
//Code #06
enum Alignment
{
Left = 1,
Center = 0,
Right = 2
}
那麼,下面這句
Alignment a = new Alignment();
將等效於
Alignment a = Alignment.Center;
3.2.2 沒有0值成員
如果枚舉類型中任何一個成員都不為0,例如
// Code #07
enum Alignment
{
Left = 1,
Center = 2,
Right = 3
}
那麼
Alignment a = new Alignment();
將等效於
Alignment a = (Alignment)0;
或者
Alignment a = 0;
而此時,枚舉變量a所儲存的值我們可以稱為非預定義枚舉(成員)值。
3.2.3 有兩個或以上的0值成員
那麼,如果枚舉類型裡存在多於一個成員被賦予0值呢?例如
// Code #08
enum Alignment
{
Left = 0,
Center = 1,
Right = 0
}
你能猜得出下面代碼的運行結果嗎?
// Code #09
Alignment a = new Alignment();
Console.WriteLine(a.ToString());
從該代碼的運行結果中我們可以看到,new把Alignment.Left“許配”給枚舉變量a。現在讓我們看看下面這段代碼:
// Code #10
string a = Enum.GetName(typeof(Alignment), 0);
Console.WriteLine(a.ToString());
其實,Code #10和Code #09的輸出結果一樣的,從.NET的源代碼中我們也可以看到,選擇對象的規則是先用Array.Sort(Array keys, Array items);對枚舉成員名稱及其值進行排序,再用循環挑選第一個出現的幸運兒。
3.3 結構類型(Struct types)的默認值
對於結構類型(Struct types),其所包含的值類型字段會被初始化為對應的默認值,而引用類型字段會被初始化為null。
// Code #11
// See Code #02 for Alignment.
public struct MyStruct
{
public int Integer;
public Alignment Align;
public string Text;
}
那麼,如下代碼:
MyStruct m = new MyStruct();
Console.WriteLine(m.Integer);
Console.WriteLine(m.Align);
Console.WriteLine(m.Text == null);
運行結果將是:
0
Left
True
4.你認為如何使用枚舉才恰當?
現在,把本文之前的都忘了,認真地考慮一下這個問題:
你認為如何使用枚舉才恰當?
嗯,這是一個非常大的問題,要對其進行較全面的回答明顯超出我的能力范圍,如果勉強為之必定贻笑大方。但我不能置之不理,於是我只好把自己的想法作拋磚引玉之用。
太極的精髓不在其招式而在其理念,沒有思想的軀體猶如行屍走肉。語言的最基本作用是交流。要成功進行交流,你得先有想法,再運用語言規則把它表達出來,傳達給你的受眾,進而達到交流的目的。至於交流的效果,就要看你對語言規則的熟悉程度和運用功底了。
程序設計語言亦然,它不但是你跟計算機交流的橋梁,更是你跟下一個代碼維護者交流的橋梁。或許你的代碼是合法的,因為計算機能讀懂它,但這不代表你的代碼是友好的,因為下一個代碼維護者可能根本摸不著頭腦。易地而處,如果你將要維護某人龍飛鳳舞的作品,你可能早早就拍台踢凳了。我認為你絕對有責任為下一個代碼維護者著想一下。再者,下一個代碼維護者也可能是你自己,不知你有否嘗過隔了一段較長的時間後再回顧自己曾經的代碼時所感到的陌生感覺?
每一種語言都有著不為人所推薦的雷區和陷阱。由於某些原因,人們不能把這些雷區和陷阱徹底清除,為了眾人,人們把這些惱人的東西給隱藏起來,而你卻偏偏要把它挖出來看個究竟。在這灰色地帶的國度裡,對與錯的界限非常模糊,很多東西既不禁止也不推薦,於是沉默可能是最好的選擇。“沉默是因為包容”,然而,這卻被你拿來當作你那句“你說給過我縱容”的理由。細細品味周傑倫的《借口》,你或許也能從中體會到其中的無奈。
我們所處的世界極其復雜,以至於到目前還不能徹底摸清它的來頭,但我們從未放過任何一個探索的機會。零散散亂的知識對我們更好的認識這個世界基本不起什麼作用,我們需要的是結構化、系統化的知識體系,以便協助我們更全面的俯瞰這個世界的微妙。合理建立分類使得我們能夠更好的整理現有的知識,每一種分類方式又從某個側面把相關的知識聯系並展現出來,使得我們能夠更加有效的運用這些相關聯的知識。
我覺得一個枚舉類型就是一種分類方法。它使得你能夠從某一側面透視你的目標群體。對於一段給定的文字(string Class),我們既可以從文字樣式(FontStyle Enumeration)的角度來看待它,也可以從對齊類型(Alignment Enumeration)的角度去分析它,還可以從顏色(KnownColor Enumeration)的角度去考察它。我們的行動應該是本著一定的目的的,如果你壓根就不知道為何要這樣做,那麼不這樣做可能是最合適的。
回顧本文上面所說的一切,你認為我們真的有必要用new來初始化枚舉變量嗎?如果枚舉代表一種分類的思想,那麼為什麼你還要冒險令枚舉變量(有可能)儲存非預定義枚舉(成員)值呢(見3.2.2節)?