首页 > 代码库 > 《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为值类型的有效状态