首页 > 代码库 > 《Effective C#》条款8:确保0为值类型的有效状态
《Effective C#》条款8:确保0为值类型的有效状态
.NET系统的默认初始化机制会将所有的对象设置为0[14]。对于值类型来讲,我们无法阻止其他程序员将其所有的成员都初始化为0[15]。因此,我们应该将0作为值类型的默认值。
枚举类型就是一种典型的情况。我们创建的枚举类型决不应该将0视为无效状态。我们知道,所有的枚举类型都继承自System.ValueType。默认的枚举值从0开始,但是我们可以更改这种默认行为。
public enum Planet
{
// 显式赋值。
// 否则将默认从0开始。
Mercury = 1,
Venus = 2,
Earth = 3,
Mars = 4,
Jupiter = 5,
Saturn = 6,
Neptune = 7,
Uranus = 8,
Pluto = 9
}
Planet sphere = new Planet();
这里的sphere将为0,显然是一个无效的状态。这样,那些要求“枚举值必须位于预定义集合中”的代码(通常都是这样的情况)就不能正常工作了。因此,当我们创建自己的枚举值时,要确保0为有效的状态。如果我们使用位模式来定义枚举值,那么应该将0定义为“不包括所有其他属性的情况”。
根据目前的情况来看,我们应该强制用户显式初始化枚举值:
Planet sphere = Planet.Mars;
但这将使得这样的枚举类型很难作为值类型的成员:
public struct ObservationData
{
Planet _whichPlanet; // 看的是什么呢?
Double _magnitude; // 感觉亮度。
}
创建ObservationData对象将得到一个无效的Planet字段:
ObservationData d = new ObservationData();
新创建的ObservationData对象的_magnitude将为0,这是合理的。但_whichPlanet却是无效的。我们需要让0成为有效的状态。如果可能的话,我们最好将0作为默认的值。Planet枚举类型没有一个明显的默认值。当用户没有给出明确的选择时,我们随便设定一个Planet值是没有意义的。如果碰到这种情况,我们可以将0作为一个未初始化值明确表示出来,这样可方便后续再对其更新:
public enum Planet
{
None = 0,
Mercury = 1,
Venus = 2,
Earth = 3,
Mars = 4,
Jupiter = 5,
Saturn = 6,
Neptune = 7,
Uranus = 8,
Pluto = 9
}
Planet sphere = new Planet();
现在sphere将包含一个None值。将这个未初始化的默认值添加到Planet枚举中,会给ObservationData结构带来一些影响。新创建的ObservationData对象将包含一个值为0的_magnitude和一个值为None的_whichPlanet。这时候,我们应该添加一个显式的构造器,来支持用户显式初始化类型所有的字段:
public struct ObservationData
{
Planet _whichPlanet; // 看的是什么呢?
Double _magnitude; // 感觉亮度。
ObservationData( Planet target,
Double mag )
{
_whichPlanet = target;
_magnitude = mag;
}
}
但是,要记住ObservationData仍然有一个默认构造器。用户仍可以使用默认的构造器来创建“让系统初始化”的变量,我们无法禁止用户这么做。
在讨论其他值类型之前,我们需要再谈一下枚举类型作为位标记(flag)来应用时的一些特殊规则。使用Flags特性的枚举类型应该总是将None值设为0:
[Flags]
public enum Styles
{
None = 0,
Flat = 1,
Sunken = 2,
Raised = 4,
}
许多开发人员都在位标记枚举值上使用“按位AND”(bitwise AND)操作符。如果遇到0值,就会出现严重的问题。如果Flat值为0,那么下面的测试将永远为false:
if ( ( flag & Styles.Flat ) != 0 ) // 如果Flat == 0,将永远为false。
DoFlatThings( );
如果使用Flags,我们要确保0为有效状态,且其意义为“不包括所有其他标记的情况”。
如果值类型中包含有引用类型,会出现另一种常见的初始化问题。包含字符串就是一种常见的情况:
public struct LogMessage
{
private int _ErrLevel;
private string _msg;
}
LogMessage MyMessage = new LogMessage( );
MyMessage对象的_msg字段将为一个空引用。我们没有办法强制做其他的初始化,但是我们可以使用属性来将该问题限定在类型内部。我们可以创建一个属性来将_msg值暴露给类型的所有客户,并在属性内部添加逻辑,使其返回一个“内容为空的字符串”,而非一个空引用:
public struct LogMessage
{
private int _ErrLevel;
private string _msg;
public string Message
{
get
{
return (_msg != null ) ?
_msg : string.Empty;
}
set
{
_msg = value;
}
}
}
我们应该在类型内部使用这样的属性。这样做可以将空引用检查集中在一个地方。当从我们的程序集中被调用时,Message的访问器方法几乎肯定会被内联。我们在获得高效代码的同时,也将错误降到了最低。
综上所述,系统会将值类型的所有实例初始化为0。我们没有办法阻止用户创建“字段全部为0”的值类型实例。如果可能的话,我们应该将“字段全部为0”作为类型的默认值。作为一种特殊情况,被用做位标记的枚举类型,应该确保0的意义为“不包括所有其他标记的情况”。
《Effective C#》条款8:确保0为值类型的有效状态