How to Embellish Enums in C#

Viewed 130

(Apologies if this sort of question should instead go on Code Review.)

I'd like to know the best way to augment an enum with additional information. The specific case is that I'm working on a game where I have an enum representing a type of Infliction, which could be physical, magical, or status related.

For example:

enum InflictionTypes { Pierce, Slash, Bruise, Heat, Shock, Cold, Metal, Salt, Wet}

In some places my combat system treats these all the same (e.g., how various armors or protections work). But in other places the combat system needs to treat them differently (e.g., the actual effect they have if they get through the armor.) So I need some way of discriminating among the kinds and am running into the limitations of Enums.

If it were possible to create a hierarchy of enums, I could do that---have a "base enum" of InflictionTypes and "derive" from that to have individual kinds. But obviously this is madness.

Alternatively, if it were possible to embellish an enum with some other piece of information, that would work, but enums obviously do not hold fields.

I'm considering a few possibilities:

Possibility A: Create a decorator class

I could make a class that holds the enum as a field and holds the other information (possibly another Enum: InflictionOutcome) as a field, property, or method. I would use a static constructor to initialize a private dictionary used to provide the information.

One mild negative with this option is that in general it may difficult to find a good name capturing the meaning of this class, which essentially has the same purpose as the enum it decorates. (In this case "Infliction" is not particularly good, as that would represent the combination of this InflictionType with some other information.)

Possibility B: Create an Enum Extension Method

Another option would be to write an extension function for the enum to provide the additional information. Codewise this seems inelegant because the extension method would either be along switch statement or require generation of the lookup dictionary inside the method.

Possibility C: Don't use an Enum

Self-explanatory. Given how this information is used in other parts of the code, it would be pretty unfortunate not to be able to have an enum.

I'm wondering if there are better options; it seems there should be.

3 Answers

I have actually already done something similar. I mapped that with attributes. I also added a context object to these attributes so that I could distinguish them on a case-by-case basis and treat them differently.

In my case, I added RESX to be able to automatically display the enum values on the interface in readable text. However, since the texts can have a different meaning depending on the context, I introduced the ContextIdentifier. This has worked quite well so far.

The whole thing can then be wonderfully supplemented with an extension method to determine the correct value or meaning on the basis of a (global) context variable.

    [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
    public class ContextAttribute : Attribute
    {
        public ContextAttribute(int operationValue, object context = null)
        {
            // TODO
        }
    }

    public enum InflictionTypes
    {
        [Context(123)]
        [Context(-100, "Fight")]
        Pierce,

        Slash, Bruise, Heat, Shock, Cold, Metal, Salt, Wet
    }

But in fact, I think it depends very much on the particular assignment whether that is a sensible way to go or not.

How about using [Flags]? Something like:

[Flags]
public enum InflictionTypes
{
    ArmourProtects = 0x0001_0000,
    Environmental = 0x0002_0000,
    Pierce = 0x0001 | ArmourProtects,
    Slash = 0x0002 | ArmourProtects,
    Bruise = 0x0004,
    Heat = 0x0008 | Environmental,
    Shock = 0x0010,
    Cold = 0x0020 | Environmental,
    Metal = 0x0040,
    Salt = 0x0080,
    Wet = 0x0100,
}

and then creating extension methods for testing things:

public static bool IsEnvironmental(this InflictionTypes anEnumValue)
{
    return (anEnumValue & InflictionTypes.Environmental) != 0;
}

The downside is that you end up mixing the values and the categories in the same type.

Update

The mixing of the values and the categories has bothered me. But, this works (use whatever strategy you want to set the bit pattern):

[Flags]
public enum InflictionGroups
{
    ArmourProtects = 0x0001_0000,
    Environmental = 0x0002_0000,
}

[Flags]
public enum InflictionTypes
{
    Pierce = 0x0001 | InflictionGroups.ArmourProtects,
    Slash = 0x0002 | InflictionGroups.ArmourProtects,
    Bruise = 0x0004,
    Heat = 0x0008 | InflictionGroups.Environmental,
    Shock = 0x0010,
    Cold = 0x0020 | InflictionGroups.Environmental,
    Metal = 0x0040,
    Salt = 0x0080,
    Wet = 0x0100,
}

One drawback is that you can't use | and & between two different enum types. However, you can do this in an extension method:

public static bool IsEnvironmental(this InflictionTypes anEnumValue)
{
    return ((int)anEnumValue & (int)InflictionGroups.Environmental) != 0;
}

Rather than use an enum, use a class with constants that behave exactly like an enum. An enum is just a wrapper of int values typically. I assume you want an API that allows you to chain enums from a state. Something like this, perhaps:

public static void Play()
{
    var stateInvincible = InflictionState.Invincible;

    var playerInjury = stateInvincible.Bruise;

    if( playerInjury == stateInvincible.Bruise)
    {
        // do stuff
    }
}

or chained in one line:

var playerInjury = InflictionState.Invincible.Bruise;

If so, use the derived InflictionTypes class as a subclass of the InflictionState class to force it's use within the state, and pass int for comparisons:

public class InflictionState
{
    private InflictionState(){} // Keeps it static-like.

    public static InflictionTypes Magic = new InflictionTypes(0);
    public static InflictionTypes Invincible = new InflictionTypes(1);
    public static InflictionTypes Vulnerable = new InflictionTypes(2);

    public class InflictionTypes
    {
        private int _state;
        public InflictionTypes(int state)
        {
            this._state = state;
        }
        public int Pierce => 100 + this._state;
        public int Slash => 200 + this._state;
        public int Bruise => 300 + this._state;
    }
}

I used 100 as the interval for the type, and 1 as the interval for the state. This allows 100 states.

Related