Better C# Enums

I hate C#’s enum type. I avoid it if I can. Consider this example of an enum of planets:

public enum Planet {
    MERCURY,
    VENUS,
    EARTH,
    MARS,
    JUPITER,
    SATURN,
    URANUS,
    NEPTUNE,
    PLUTO // Pluto is a planet!!!
}

This is good enough to use for a while until a need arises to get the mass of a planet. So we do something like this:

// Returns the mass of the planet in 10^24 kg
public float GetMass(Planet planet) {
    switch(planet) {
        case Planet.MERCURY:
            return 0.330;

        case Planet.VENUS:
            return 4.87f;

        case Planet.EARTH:
            return 5.97f;

        ...

        case Planet.PLUTO:
            return 0.0146f;
    }
}

How about planet diameter? Another switch statement? What about for density? Gravity? Escape velocity? Just think about the amount of switch statements that you’re going to maintain. You can argue that you can use a Dictionary instead but that is still clunky. A Dictionary mapping per data? No way.

There is a better way and I’ll show you how. This may already be common knowledge among non Unity programmers but I’d like to put up this redundant topic again in my blog for such people who may not know this, especially beginners. I would also like to keep it simple. There will be no need for reflection in my examples. There’s also no inheritance.

Basically, you can use a class as an enum. Why a class? It’s just better. You can store any number of arbitrary data. You can even store a routine or a function. You can do a lot with it. The only requirement is that it should be immutable, which means that the state of an instance of the class cannot change throughout the duration of the program. Here’s a version of the Planet enum as a class:

    public class Planet {
        // The different values
        public static readonly Planet MERCURY = new Planet(0, 0.330f, 4879, 5427, 3.7f);
        public static readonly Planet VENUS = new Planet(1, 4.87f, 12104, 5243, 8.9f);
        public static readonly Planet EARTH = new Planet(2, 5.97f, 12756, 5514, 9.8f);
        public static readonly Planet MARS = new Planet(3, 0.642f, 6792, 3933, 3.7f);
        public static readonly Planet JUPITER = new Planet(4, 1898.0f, 142984, 1326, 23.1f);
        public static readonly Planet SATURN = new Planet(5, 568.0f, 120536, 687, 9.0f);
        public static readonly Planet URANUS = new Planet(6, 86.8f, 51118, 1271, 8.7f);
        public static readonly Planet NEPTUNE = new Planet(7, 102.0f, 49528, 1638, 11.0f);
        public static readonly Planet PLUTO = new Planet(8, 0.0146f, 2370, 2095, 0.7f);

        // Use readonly to maintain immutability
        private readonly int id;
        private readonly float mass; // in 10^24 kg
        private readonly int diameter; // in km
        private readonly int density; // in kg/m^3
        private readonly float gravity; // in m/s^2

        // We use a private constructor because this should not be instantiated
        // anywhere else.
        private Planet(int id, float mass, int diameter, int density, float gravity) {
            this.id = id;
            this.mass = mass;
            this.diameter = diameter;
            this.density = density;
            this.gravity = gravity;
        }

        public int Id {
            get {
                return id;
            }
        }

        public float Mass {
            get {
                return mass;
            }
        }

        public int Diameter {
            get {
                return diameter;
            }
        }

        public int Density {
            get {
                return density;
            }
        }

        public float Gravity {
            get {
                return gravity;
            }
        }
    }

To maintain immutability, all member variables should be readonly. Once they are assigned, they can no longer be changed. This is important because as an enum, it’s internal values should not change. Each enum value is then implemented as a static readonly instance of the class.

How is this used? It’s the same with normal enum but there’s more:

// Use it like an enum
ship.TargetPlanet = Planet.NEPTUNE;

// Want to know the target planet's mass?
float mass = ship.TargetPlanet.Mass;

// Density?
int density = ship.TargetPlanet.Density;

We have eliminated the need for switch statements or dictionaries to maintain the different planets’ information. Want a new planet stat? Just add a new member variable and specify them on instantiation.

How about conversion from other data types? Like say convert from int id to Planet instance? This is easy. Usually I add a public and static method for these conversions. For example:

public class Planet {

    // The different values
    public static readonly Planet MERCURY = new Planet(0, 0.330f, 4879, 5427, 3.7f);
    public static readonly Planet VENUS = new Planet(1, 4.87f, 12104, 5243, 8.9f);
    public static readonly Planet EARTH = new Planet(2, 5.97f, 12756, 5514, 9.8f);
    public static readonly Planet MARS = new Planet(3, 0.642f, 6792, 3933, 3.7f);
    public static readonly Planet JUPITER = new Planet(4, 1898.0f, 142984, 1326, 23.1f);
    public static readonly Planet SATURN = new Planet(5, 568.0f, 120536, 687, 9.0f);
    public static readonly Planet URANUS = new Planet(6, 86.8f, 51118, 1271, 8.7f);
    public static readonly Planet NEPTUNE = new Planet(7, 102.0f, 49528, 1638, 11.0f);
    public static readonly Planet PLUTO = new Planet(8, 0.0146f, 2370, 2095, 0.7f);

    // This can be used to loop through all planets
    public static Planet[] ALL = new Planet[] {
        MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE, PLUTO
    };

    // Converts the specified id to a Planet instance
    public static Planet Convert(int id) {
        for(int i = 0; i < ALL.Length; ++i) {
            if(ALL[i].Id == id) {
                return ALL[i];
            }
        }

        // return ALL[id] could also work here but what if a non sequential id is used?

        throw new Exception("Cannot convert {0} to a Planet.".FormatWith(id));
    }

    ...
}

// Usage
Planet planet = Planet.Convert(someIntPlanet);

Want to convert from a string id? Add a string member variable that will hold this value. Instead of using an array such as ALL[], you can use a Dictionary like this:

private static Dictionary<string, Planet> ALL = new Dictionary<string, Planet>() {
    { MERCURY.TextId, MERCURY },
    { VENUS.TextId, VENUS },
    { EARTH.TextId, EARTH },
    ...
    { PLUTO.TextId, PLUTO },
};

// Converts the specified string to a Planet instance
public static Planet Convert(string id) {
    return ALL[id];
}

You can support any type of conversion that you like.

There’s so much more you can do. You can now add functions. You can do something like this:

Planet currentPlanet = Planet.VENUS;
currentPlanet.ApplyGravity(ship);

The coolest thing for me is you can specify different actions or behavior to the enum values. Something like this (It’s very contrived but you get the idea.):

public static readonly Planet EARTH = new Planet(2, 5.97f, 12756, 5514, 9.8f, delegate(Ship ship) {
    // Actions on land of ship
    ship.AddFood(1000);
    ship.RetireCrew();
    ship.RecruitNewCrew();
});

public static readonly Planet MARS = new Planet(3, 0.642f, 6792, 3933, 3.7f, delegate(Ship ship) {
    // Actions on land of ship
    ship.DeductFood(50);
    ship.Research();
    ship.Mine();
});

By simply turning your enum into a class, you’ve upgraded it to something more organized yet also more feature packed. You could also use advance features like reflection and inheritance, but most of the time, you don’t need to.

That’s it for now. Hope this helps.

3 thoughts on “Better C# Enums

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s