从枚举的初始化说起 [C#]
从枚举的初始化说起 [C#]
Written by Allen Lee
我知道你的痛,是我给的承诺。你说给过我纵容,沉默是因为包容。如果要走,请你记得我;如果难过,请你忘了我。——周杰伦,《借口》
0. 缘起
本文写作缘于netwy的《枚举类型的变量的默认值一定是 0 !》。
1. 问题
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指定第一个成员的值:
enum Alignment
{
Left,
Center,
Right
}
Code #01的输出结果将是:
0
0
我们再把Code #01的Main反编译成IL:
.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::ToString(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::ToString(string)
L_002b: call void [mscorlib]System.Console::WriteLine(string)
L_0030: nop
L_0031: ret
}
从上面的代码中,我们可以看到这两种初始化方式是等效的。实质上,下面这4句(在此时)是等效的(它们产生一样的IL代码):
Alignment b = Alignment.Left;
Alignment d = (Alignment)0;
Alignment c = 0;
2.2 第一个枚举成员的值非0
如果我们手动指定Alignment的第一个成员的值呢?
enum Alignment
{
Left = 1,
Center,
Right
}
Code #01的输出结果将有点令人疑惑:
0
1
为什么会这样呢?让我们从IL代码中看看编译器是如何理解此时的Main的:
.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::ToString(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::ToString(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
也就是说,使用枚举的第一个成员来初始化枚举变量,编译器懂得根据枚举的定义来作出相应的调整,从而编译出符合我们预期的代码。这种处理方式实际上使用了多态性的思维。
而此时,
相当于
或者
3. new、值类型的默认构造函数和值类型的默认值
通常我们使用new来调用引用类型的实例构造函数(Instance Constructors),或者自定义值类型的非默认实例构造函数(Non-Default Instance Constructors)。然而,我们也可以使用new来调用值类型(包括内置简单类型和自定义类型)的默认构造函数,例如:
这里,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的成员被赋值如下:
enum Alignment
{
Left = 1,
Center = 0,
Right = 2
}
那么,下面这句
将等效于
3.2.2 没有0值成员
如果枚举类型中任何一个成员都不为0,例如
enum Alignment
{
Left = 1,
Center = 2,
Right = 3
}
那么
将等效于
或者
而此时,枚举变量a所储存的值我们可以称为非预定义枚举(成员)值。
3.2.3 有两个或以上的0值成员
那么,如果枚举类型里存在多于一个成员被赋予0值呢?例如
enum Alignment
{
Left = 0,
Center = 1,
Right = 0
}
你能猜得出下面代码的运行结果吗?
Alignment a = new Alignment();
Console.WriteLine(a.ToString());
从该代码的运行结果中我们可以看到,new把Alignment.Left“许配”给枚举变量a。现在让我们看看下面这段代码:
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。
// See Code #02 for Alignment.
public struct MyStruct
{
public int Integer;
public Alignment Align;
public string Text;
}
那么,如下代码:
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节)?
在本文结束之时,我想说
东西是否有用要看你是否会用;东西是否有效要看你是否用对。请使用语言有利的一面来协助我们的工作,而不是使用其有害的一面来伤害自己和别人。
See Also:
- Allen Lee,《关于枚举的种种 [C#, IL, BCL]》