Effective C# 原则8:确保0对于值类型数据是有效的

.Net系统默认所有的对象初始化时都为0。这并没有提供一个方法来预防其他程序员创建的值类型数据的实例在初始化是都是0。请让你的数据类型默认值也是0。

一个特殊情况是在枚举类型数据中。决不要创建一个不包括0在内的枚举类型。所有的枚举类型都是从System.ValueType派生的。枚举类型的值是从0开始的,但你可以改变这一行为:

public enum Planet
{
  // Explicitly assign values.
  // Default starts at 0 otherwise.
  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是当中的一个。如果你的枚举类型采用的是以位(bit)模式,把0定义为其它属性不存在时的取值。

按照现在的情况,你迫使用户必须精确的初始化值:

Planet sphere = Planet.Mars;

这将使包含(Planet)这一类型的其它类型很难创建:

public struct ObservationData
{
  Planet   _whichPlanet; //what am I looking at?
  Double  _magnitude; // perceived brightness.
}

创建一个新ObservationData实例的用户会创建一个不合法的Planet成员:

ObservationData d = new ObservationData();

最后创建的ObservationData的成员_magnitude的值是0,这是合理的。但_whichPlanet却是无效的。你须要让0也是有效的(状态)。如果可能,选择把0做为一个最好的默认。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对象的目标上具有None和一个数值0。添加一个清晰的构造函数让用户为你的类型的所有字段明白的初始化:

public struct ObservationData
{
  Planet   _whichPlanet; //what am I looking at?
  Double  _magnitude; // perceived brightness.

  ObservationData( Planet target,
    Double mag )
  {
    _whichPlanet = target;
    _magnitude = mag;
  }
}

但请记住,默认的构造函数还是可访问的,而且是结构的部份。用户还是可以创建一个系统初始化的变量,而你无法阻止它。

在结束枚举类型转而讨论其它类型之前,你须要明白几个用于标记的特殊枚举类型规则。枚举类型在使用Flags特性时,必须把None的值设置为0:

[Flags]
public enum Styles
{
  None = 0,
  Flat = 1,
  Sunken = 2,
  Raised = 4,
}

很多开发人员使用枚举标记和位运算操作AND进行运行,0值会与位标记产生严重的问题。下面这个实验如果Flat的值是0时,是决不会成功的:

if ( ( flag & Styles.Flat ) != 0 ) // Never true if Flat == 0.
  DoFlatThings( );

如果你遇到Flags,确保0对它来说是有效的,并且这就着:“对所有缺少的标记。”

另一个很常见的初始化问题就是值类型中包含了引用类型。字符串是一个常见的例子:

public struct LogMessage
{
  private int _ErrLevel;
  private string _msg;
}

LogMessage MyMessage = new LogMessage( );

MyMessage包含了一个_msg为null的引用字段。这里没有办法强行使用另一个不同的初始化方法,但你利用属性来局部化这个问题。你创建一个属性向所用的用户暴露_Msg的值。添加一个业务逻辑,使得当字符串为null引用是,用空 串来取而代之:

public struct LogMessage
{
  private int _ErrLevel;
  private string _msg;

  public string Message
  {
    get
    {
      return (_msg != null ) ?
        _msg : string.Empty;
    }
    set
    {
      _msg = value;
    }
  }
}

(译注:我个人觉得这里违反了原则一。当对两个实例进行赋值COPY时,会出现,你明明使用了a=b的运行,但实际上a!=b的结果。可以参见原则1。)

在你自己的数据类型内部,你应该添加这样的一个属性。做了这样的局部处理后,null引用在某一位置做了验证。当调用是在你的程序集内时,Message的访问器基本上是可以很好的内联的。你将会取得高效低错的代码。

系统为所有的值类型数据初始化为0,而没有办法防止用户在创建一个值类型实例时,给所有的值类型都赋值为0。如果可能,把0设置为自然的默认值。特殊情况下,使用Flags特性的枚举类型必须确保0是所有缺省标记的值。

========================================================
The default .NET system initialization sets all objects to all 0s. There is no way for you to prevent other programmers from creating an instance of a value type that is initialized to all 0s. Make that the default value for your type.

One special case is enums. Never create an enum that does not include 0 as a valid choice. All enums are derived from System.ValueType. The values for the enumeration start at 0, but you can modify that behavior:

public enum Planet
{
  // Explicitly assign values.
  // Default starts at 0 otherwise.
  Mercury = 1,
  Venus = 2,
  Earth = 3,
  Mars = 4,
  Jupiter = 5,
  Saturn = 6,
  Neptune = 7,
  Uranus = 8,
  Pluto = 9
}

Planet sphere = new Planet();

sphere is 0, which is not a valid value. Any code that relies on the (normal) fact that enums are restricted to the defined set of enumerated values won't work. When you create your own values for an enum, make sure that 0 is one of them. If you use bit patterns in your enum, define 0 to be the absence of all the other properties.

As it stands now, you force all users to explicitly initialize the value:

Planet sphere = Planet.Mars;

That makes it harder to build other value types that contain this type:

public struct ObservationData
{
  Planet   _whichPlanet; //what am I looking at?
  Double  _magnitude; // perceived brightness.
}

Users who create a new ObservationData object will create an invalid Planet field:

ObservationData d = new ObservationData();

The newly created ObservationData has a 0 magnitude, which is reasonable. But the planet is invalid. You need to make 0 a valid state. If possible, pick the best default as the value 0. The Planet enum does not have an obvious default. It doesn't make any sense to pick some arbitrary planet whenever the user does not. When you run into that situation, use the 0 case for an uninitialized value that can be updated later:

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 now contains a value for None. Adding this uninitialized default to the Planet enum ripples up to the ObservationData structure. Newly created ObservationData objects have a 0 magnitude and None for the target. Add an explicit constructor to let users of your type initialize all the fields explicitly:

public struct ObservationData
{
  Planet   _whichPlanet; //what am I looking at?
  Double  _magnitude; // perceived brightness.

  ObservationData( Planet target,
    Double mag )
  {
    _whichPlanet = target;
    _magnitude = mag;
  }
}

But remember that the default constructor is still visible and part of the structure. Users can still create the system-initialized variant, and you can't stop them.

Before leaving enums to discuss other value types, you need to understand a few special rules for enums used as flags. Enums that use the Flags attribute should always set the None value to 0:

[Flags]
public enum Styles
{
  None = 0,
  Flat = 1,
  Sunken = 2,
  Raised = 4,
}

Many developers use flags enumerations with the bitwise AND operator. 0 values cause serious problems with bitflags. The following test will never work if Flat has the value of 0:

if ( ( flag & Styles.Flat ) != 0 ) // Never true if Flat == 0.
  DoFlatThings( );

If you use Flags, ensure that 0 is valid and that it means "the absence of all flags."

Another common initialization problem involves valuetypes that contain references. Strings are a common example:

public struct LogMessage
{
  private int _ErrLevel;
  private string _msg;
}

LogMessage MyMessage = new LogMessage( );

MyMessage contains a null reference in its _Msg field. There is no way to force a different initialization, but you can localize the problem using properties. You created a property to export the value of _Msg to all your clients. Add logic to that property to return the empty string instead of null:

public struct LogMessage
{
  private int _ErrLevel;
  private string _msg;

  public string Message
  {
    get
    {
      return (_msg != null ) ?
        _msg : string.Empty;
    }
    set
    {
      _msg = value;
    }
  }
}

You should use this property inside your own type. Doing so localizes the null reference check to one location. The Message accessor is almost certainly inlined as well, when called from inside your assembly. You'll get efficient code and minimize errors.

The system initializes all instances of value typess to 0. There is no way to prevent users from creating instances of value types that are all 0s. If possible, make the all 0 case the natural default. As a special case, enums used as flags should ensure that 0 is the absence of all flags.

评论: 0 | 引用: 0 | 查看次数: 3554
发表评论
登录后再发表评论!