How to deal with enumeration 0 in C# (CA1008 discussion)

喜欢而已 提交于 2020-06-23 02:36:49

问题


Rule CA1008 specifies that all enumerations should have a 0 value that should be named Unknown (we are not discussing flags here). I understand the reason that you want to prevent that uninitialized values would automatically get a meaning. Suppose I define the following enumeration:

enum Gender
{
    Male,
    Female
}

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

This specifies that each person should either be male or female (let's leave out the gender discussion for now). If I forget to set the Gender property, then this person is automatically male, which could cause problems. For that reason, I understand the CA1008 warning, so the 0 value should be reserved for an unknown/uninitialized value.

So let's change the Gender enumeration to and not use the 0 value anymore:

enum Gender
{
    Male = 1,
    Female = 2
}

When I don't specify the gender, then the person isn't male or female. Problems may arise during serialization. The value 0 isn't very descriptive for an enumeration during debugging. To fix it, and avoid the CA1008 warning, I change the enumeration again:

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

Uninitialized properties now show up as Unknown which looks good. But I may have introduced another issue and that is that the Unknown value looks like a valid value and can be applied to a user. I also might get warnings about not dealing with all enumeration values. Suppose, I use constructor that requires me to specify the gender and a name to avoid uninitialized properties:

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

When I define the Unknown enumeration, then I now can explicitly set the gender to Unknown. Of course this can be checked in the constructor, but this will only be signalled at run-time. When the Unknown value isn't defined, then the caller could only set it to male or female.

A fix may be to use a nullable gender property, so the uninitialized value is an explicit null value (we don't define the Unknown value anymore). But using nullable types makes programming more complex, so I wouldn't advise it.

Applying the ObsoleteAttribute to the Unknown value might be a good idea. When somebody explicitly uses the value, then it is flagged as a warning (at build-time).

What is the proper way to deal with the uninitialized enumeration values and is using the ObsoleteAttribute a good idea or does it have other drawbacks?

Notes: * Although obsolete isn't the proper semantics here, it is the only (easy) way to generate a warning if the value is used. * Using POCO without a default constructor may complicate serialization, so it's generally a bad idea to have (serializable) classes without them.


回答1:


is using the ObsoleteAttribute a good idea?

No. Use [Obsolete] to … wait for it … mark an obsolete member as obsolete. That is the only correct usage of [Obsolete]. Don't go inventing new meanings for existing words; that just creates confusion.

What is the proper way to deal with the uninitialized enumeration values?

That's the wrong question. Take a step back. Let's look at the larger picture of your question:

  1. You decided to use an enum
  2. You got a bunch of warnings saying that your enum violated some guidelines, and you decided the guidelines were important
  3. Every attempt you made to follow the guidelines produced a new problem, and you decided those problems were important.

Now you're stuck, wondering what to do.

What you do is: go back to steps one through three and make different decisions.

Suppose we revisit decision 3. You've identified a bunch of pros and cons of each solution. Decide that for one of them, the pros outweigh the cons, and go with it.

Suppose we revisit decision 2. You've violated a bunch of guidelines. Guidelines are not rules. You can decide that the guidelines are bad advice for your scenario, document the fact that you're deliberately violating them and why, suppress the warning, and go forth.

Suppose we revisit decision 1. You're the one who decided that gender is best represented as an enum, and it seems like that decision has caused you considerable pain since. So reject that decision:

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
}

And what have we got? We've got Gender.Male and Gender.Female same as before, they're serializable same as before, and any value of type Gender is either male, female or null. Don't like nulls? Throw an exception, same as if you got a null string for the person's name. Want to add more genders, like "Unknown" or "Nonbinary" or whatever? Add new subtypes and static fields.

You aren't being forced to use an enum. If meeting the guidelines for an enum is vexing you, stop using enums.




回答2:


The goal of having an enumeration is to provide named constants that represent the possible values. In your particular design (which is an abstraction of the real world), the gender of a person is either Male or Female. There is no None.

Your enumeration needs a 0 value member because the default underlying type is int. For this reason (and as the compiler points out), it should should be one of your choices (Male or Female):

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

or

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

or

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

The fact that a value is required should then be enforced by the constructor of your abstraction as a person must have a gender:

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

If, on the other hand, your design abstracts a world where a person can choose not to provide their gender, then you can have the default value represent that:

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

or

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

In which case, it would make sense to have two flavors of constructors:

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

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

In other words, the correct handling is already indicated by the compiler. You just need to make sure your abstraction implements it in a way that properly represents what is being modeled.




回答3:


Your question defines three requirements that I believe are mostly incompatible with each other:

  1. You want the CA1008 warning to be avoided by having a 0-valued default in the enum.
  2. You want to prevent the user of enum Gender from being able to explicitly use an unknown value.
  3. You want to implement a default constructor for your Person class.

If you satisfy 1 & 2 then you must introduce a non-default constructor to enforce the initialization of Gender. Then 3 can't be satisfied.

If you satisfy 1 & 3 then you must accept that the user could forget to initialize this property and you must introduce something to handle the case where the property has a valid value but has been left uninitialized. In many cases the solution would be implementing and handling the Unknown default as a third valid value, but then 2 can't be satisfied.

If you satisfy 2 & 3 then you run into the problem where you have to decide wether Gender should be initialized to Male or Female by default to also satisfy #1. If the default constructor is used you will run in to problems when the default choice is wrong half of the time.

The only way forward to satisfy all three of these requirements may be to model Gender as a sub-type of Person rather than just a property.

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()
    { ... }
}

In this way you are enforcing that the user must instantiate a Person using either the Male default constructor or the Female default constructor. Serialization would also be able to preserve the sub-types and use default constructors without resulting in an incorrect default value.



来源:https://stackoverflow.com/questions/54561566/how-to-deal-with-enumeration-0-in-c-sharp-ca1008-discussion

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!