规则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可能会使序列化复杂化,因此拥有(可序列化的)类而没有它们通常是一个坏主意。
答案 0 :(得分:4)
使用ObsoleteAttribute是个好主意吗?
不。使用[Obsolete]
…等待它…将一个过时的成员标记为过时。那是[Obsolete]
的 only 正确用法。不要为现有单词发明新的含义;只会造成混乱。
处理未初始化的枚举值的正确方法是什么?
这是错误的问题。退后一步。让我们看一下问题的大图:
现在您被困住了,想知道该怎么办。
您要做的是:返回第一步到第三步并做出不同的决定。
假设我们重新审视第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.Male
和Gender.Female
与以前相同,它们的可序列化与以前相同,并且类型Gender
的任何值可以是男性,女性或null。不喜欢空值?抛出一个异常,就像您为该人的名字得到一个空字符串一样。是否要添加更多性别,例如“未知”或“非二进制”或其他?添加新的子类型和静态字段。
您不会被迫使用枚举。如果满足枚举准则使您感到烦恼,请停止使用枚举。
答案 1 :(得分:2)
进行枚举的目的是提供表示可能值的命名常量。在您的特定设计(这是对现实世界的抽象)中,一个人的性别是Male
或Female
。没有None
。
您的枚举需要一个0
值成员,因为默认基础类型为int
。因此(正如编译器指出的那样),它应该是您的选择之一(Male
或Female
):
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和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。 序列化还可以保留子类型并使用默认构造函数,而不会导致错误的默认值。