简体   繁体   中英

C# enum type-safety

Is there a way to force the C# enum to only accept one of the several explicitly named constants, or is there another feature that does? The C# reference has this afterthought:

It is possible to assign any arbitrary integer value to an enum type. However, you should not do this because the implicit expectation is that an enum variable will only hold one of the values defined by the enum. To assign an arbitrary value to a variable of an enumeration type is to introduce a high risk for errors.

(A new language was designed that allows this sort of sloppiness. That is baffling to me.)

As far as I know, you can't stop C# from allowing conversions between enums and integers.

As a workaround, you could instead use a custom type with restricted instantiation. The usage will look similar, but you will also have the opportunity to define methods and operators for this type.

Say you have this enum:

enum DayOfWeek
{
    Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday 
}

You could use a sealed class in its place. The advantage is that you get comparisons for free (because a value comparison and a reference comparison are equivalent for this case). The disadvantage is that (like all reference types in C#), it's nullable.

sealed class DayOfWeek
{
    public static readonly DayOfWeek Monday = new DayOfWeek(0);
    public static readonly DayOfWeek Tuesday = new DayOfWeek(1);
    public static readonly DayOfWeek Wednesday = new DayOfWeek(2);
    public static readonly DayOfWeek Thursday = new DayOfWeek(3);
    public static readonly DayOfWeek Friday = new DayOfWeek(4);
    public static readonly DayOfWeek Saturday = new DayOfWeek(5);
    public static readonly DayOfWeek Sunday = new DayOfWeek(6);

    private readonly int _value;

    private DayOfWeek(int value) 
    {
        _value = value;
    }
}

Or you could use a struct. The advantage is that it's not nullable and thus it's even more similar to an enum. The disadvantage is that you have to implement the comparison code manually:

struct DayOfWeek
{
    public static readonly DayOfWeek Monday = new DayOfWeek(0);
    public static readonly DayOfWeek Tuesday = new DayOfWeek(1);
    public static readonly DayOfWeek Wednesday = new DayOfWeek(2);
    public static readonly DayOfWeek Thursday = new DayOfWeek(3);
    public static readonly DayOfWeek Friday = new DayOfWeek(4);
    public static readonly DayOfWeek Saturday = new DayOfWeek(5);
    public static readonly DayOfWeek Sunday = new DayOfWeek(6);

    private readonly int _value;

    private DayOfWeek(int value)
    {
        _value = value;
    }

    public bool Equals(DayOfWeek other)
    {
        return _value == other._value;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj))
        {
            return false;
        }
        return obj is DayOfWeek && Equals((DayOfWeek)obj);
    }

    public override int GetHashCode()
    {
        return _value;
    }

    public static bool operator ==(DayOfWeek op1, DayOfWeek op2)
    {
        return op1.Equals(op2);
    }

    public static bool operator !=(DayOfWeek op1, DayOfWeek op2)
    {
        return !(op1 == op2);
    }
}

The ability to cast any integer to an enum is mostly because of performance reasons, but an enum implemented as a value type simply can't be protected from containing undefined values. Consider an enum like:

public enum Condition {
  Right = 1,
  Wrong = 2
}

Even if assignments to an enum variable was restricted to only defined values, you can still create an undefined value by simply putting it in a class:

public class Demo {
  public Condition Cond;
}

When you create an instance of the class, the members are initialized to zero, so the Cond member variable would have the undefined value of (Condition)0 .

You can make a wrapper class to make sure that an enum value is among the defined values:

public sealed class SafeEnum<T> where T : struct {

  public T Value { get; private set; }

  public SafeEnum(T value) {
    if (!(value is Enum)) {
      throw new ArgumentException("The type is not an enumeration.");
    }
    if (!Enum.IsDefined(typeof(T), value)) {
      throw new ArgumentException("The value is not defined in the enumeration.");
    }
    Value = value;
  }

}

Example:

var cond = new SafeEnum<Condition>(Condition.Right); // works
Condition d = cond.Value;

var cond = new SafeEnum<int>(42); // not an enum

var cond = new SafeEnum<Condition>((Condition)42); // not defined

An instance of the class can only contain a value that is defined in the enum, as the constructor won't allow the creation of the instance otherwise.

As the class is immutable, the value can't change to an undefined value.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM