如何在C#中处理枚举0(CA1008讨论)

时间:2019-02-06 19:56:02

标签: c# enums code-analysis

规则CA1008指定所有枚举都应有一个0值,应将其命名为Unknown(在此不讨论标志)。我了解您要防止未初始化的值会自动获得含义的原因。假设我定义了以下枚举:

enum Gender
{
    Male,
    Female
}

class Person
{
    public string Name { get; set; } 
    public Gender Gender { get; set; }
}

这指定每个人都应该是男性还是女性(现在暂时不讨论性别问题)。如果我忘记设置Gender属性,则此人将自动为男性,这可能会引起问题。因此,我理解CA1008警告,因此应将0值保留给未知/未初始化的值。

因此,让我们将Gender枚举更改为并且不再使用0值:

enum Gender
{
    Male = 1,
    Female = 2
}

如果我未指定性别,则此人不是男性还是女性。序列化过程中可能会出现问题。值0对于调试期间的枚举不是很描述。要解决此问题并避免出现CA1008警告,我再次更改了枚举:

enum Gender
{
    Unknown = 0,
    Male = 1,
    Female = 2
}

未初始化的属性现在显示为Unknown,看起来不错。但是我可能已经介绍了另一个问题,那就是Unknown值看起来像一个有效值,可以应用于用户。我也可能会收到有关未处理所有枚举值的警告。假设我使用的构造函数要求我指定性别和名称,以避免未初始化的属性:

public Person(string name, Gender gender)
{
     Name = name ?? throw new ArgumentNullException(name);
     Gender = gender;
}

当我定义Unknown枚举时,现在可以将性别显式设置为Unknown。当然可以在构造函数中检查它,但这只会在运行时发出信号。如果未定义Unknown值,则呼叫者只能将其设置为男性或女性。

一种解决方法是使用可为空的性别属性,因此未初始化的值是显式的null值(我们不再定义Unknown值)。但是使用可为空的类型会使编程更加复杂,所以我不建议这样做。

ObsoleteAttribute应用于Unknown值可能是一个好主意。当有人明确使用该值时,它会被标记为警告(在构建时)。

处理未初始化的枚举值的正确方法是什么?使用ObsoleteAttribute是个好主意还是有其他缺点?

注意: *尽管过时在这里不是正确的语义,但是如果使用了值,这是生成警告的唯一(简便)方法。 *在没有默认构造函数的情况下使用POCO可能会使序列化复杂化,因此拥有(可序列化的)类而没有它们通常是一个坏主意。

3 个答案:

答案 0 :(得分:4)

  

使用ObsoleteAttribute是个好主意吗?

不。使用[Obsolete]…等待它…将一个过时的成员标记为过时。那是[Obsolete] only 正确用法。不要为现有单词发明新的含义;只会造成混乱。

  

处理未初始化的枚举值的正确方法是什么?

这是错误的问题。退后一步。让我们看一下问题的大图:

  1. 您决定使用枚举
  2. 您收到一堆警告,指出您的枚举违反了某些准则,并且您认为准则很重要
  3. 您每次尝试遵循准则都会产生一个新问题,并且您认为这些问题很重要。

现在您被困住了,想知道该怎么办。

您要做的是:返回第一步到第三步并做出不同的决定

假设我们重新审视第3个决定。您已经确定了每种解决方案的优缺点。决定对其中一个不利因素胜过不利因素,并坚持下去。

假设我们重新审查第2个决定。您违反了许多准则。准则不是规则。您可以确定准则对于您的情况不是很好的建议,记录下您故意违反准则的事实以及原因,抑制警告,然后继续。

假设我们重新审视第1项决定。您是决定以性别最好地代表一个枚举的人,自那以后,该决定似乎给您造成了极大的痛苦。因此,请拒绝该决定:

abstract class Gender : 
   whatever interfaces you need for serialization and so on
{
  private Gender() { } // prevent subclassing 
  private class MaleGender : Gender 
  {
    // Serialization code for male gender
  }
  public static readonly Gender Male = new MaleGender();
  // now do it all again for FemaleGender
}

那我们得到了什么? Gender.MaleGender.Female与以前相同,它们的可序列化与以前相同,并且类型Gender的任何值可以是男性,女性或null。不喜欢空值?抛出一个异常,就像您为该人的名字得到一个空字符串一样。是否要添加更多性别,例如“未知”或“非二进制”或其他?添加新的子类型和静态字段。

您不会被迫使用枚举。如果满足枚举准则使您感到烦恼,请停止使用枚举。

答案 1 :(得分:2)

进行枚举的目的是提供表示可能值的命名常量。在您的特定设计(这是对现实世界的抽象)中,一个人的性别是MaleFemale。没有None

您的枚举需要一个0值成员,因为默认基础类型为int。因此(正如编译器指出的那样),它应该是您的选择之一(MaleFemale):

public enum Gender
{
    Male, //compiler defaults to 0
    Female
}

public enum Gender
{
    Male = 0,
    Female = 1
}

public enum Gender
{
    Male = 1,
    Female = 0
}

抽象值的构造函数应强制要求值,因为一个人必须具有性别:

public Person(string name, Gender gender)
{
     Name = name ?? throw new ArgumentNullException(name);
     Gender = gender;
}

另一方面,如果您的设计抽象了一个世界,人们可以选择来提供性别,那么您可以使用默认值来表示:

public enum Gender
{
    NotProvided,  //compiler defaults to 0
    Male,
    Female
}

public enum Gender
{
    NotProvided = 0
    Male = 1,
    Female = 2
}

在这种情况下,具有两种构造方法将很有意义:

public Person(string name)
{
     Name = name ?? throw new ArgumentNullException(name);
}

public Person(string name, Gender gender)
{
     Name = name ?? throw new ArgumentNullException(name);
     Gender = gender;
}

换句话说,编译器已经指示了正确的处理方式。您只需要确保抽象以正确表示要建模的内容的方式实现即可。

答案 2 :(得分:0)

您的问题定义了三个我认为彼此基本不兼容的要求:

  1. 您希望通过在枚举中使用默认值为0的值来避免CA1008警告。
  2. 您要阻止枚举Gender的用户能够显式使用未知值。
  3. 您要为Person类实现默认的构造函数。

如果满足1和2,则必须引入非默认构造函数来强制执行Gender的初始化。那三个就不能满足了。

如果满足1和3,则必须接受用户可能会忘记初始化此属性,并且必须引入 something 来处理该属性具有有效值但尚未初始化的情况。在许多情况下,解决方案是将“未知”默认值实现并处理为第三个有效值,但是不能满足2。

如果您满足2&3,则会遇到必须决定是否将性别默认初始化为“男性”或“女性”以同时满足#1的问题。如果使用默认构造函数,则当默认选择的一半时间错误时,您将遇到问题。

满足所有这三个要求的唯一方法可能是将Gender建模为Person的子类型,而不只是财产。

enum Gender
{
    Male,
    Female
}

abstract class Person
{
    public string Name { get; set; }
    public abstract Gender Gender { get; }
}

class MalePerson : Person
{
    public override Gender Gender { get { return Gender.Male; } }

    public MalePerson()
    { ... }
}

class FemalePerson : Person
{
    public override Gender Gender { get { return Gender.Female; } }

    public FemalePerson()
    { ... }
}

通过这种方式,您强制用户必须使用Male默认构造函数或Female默认构造函数实例化Person。 序列化还可以保留子类型并使用默认构造函数,而不会导致错误的默认值。