I said I’d blog about how exactly I use enums, but there are a lot of interesting issues here, so I think I’ll develop my approach over the course of a few posts.  Here’s my simplest form of class-based enum:

public class EnumType {
    public static readonly EnumType One = new EnumType("One");
    public static readonly EnumType Two = new EnumType("Two");
    // You can add as many instances as you want, but I’ll stick to two for the sake of brevity

    private string _name;

    private EnumType(string name) {
        _name = name;
    }
    
    public string Name {
        get {
            return _name;
        }
    }
}

This approach solves one problem C#’s built-in enum types:  because the EnumType constructor is private, no one else can ever create another instance.  If EnumType.One and EnumType.Two are all you create in the class, then you know that there will be no other instances of EnumType.

This style of enum is good for simple enumerations where all you’re doing is differentiating between a several options and maybe displaying a value depending on which option you pick.  To check which value of the enum you have, you can just use reference equality and chained if/else blocks, which are admittedly less performant than a switch statement, but not notably more difficult to code.

The next step is a simple composite pattern, which I’m sure you can all guess, so I’ll go on with that right now:

public abstract class EnumType {
    public static readonly EnumType One = new TypeOne();
    public static readonly EnumType Two = new TypeTwo();
    // ...

    private EnumType() {
    }

    public abstract void DoSomething();

    public abstract string Name { get; }

    private class TypeOne : EnumType {
        public override void DoSomething() {
            Console.WriteLine("I am #1!");
        }
        
        public override string Name {
            get {
                return "One";
            }
        }
    }
    
    private class TypeTwo : EnumType {
        public override void DoSomething() {
            Console.WriteLine("I am #2!");
        }
        
        public override string Name {
            get {
                return "Two";
            }
        }
    }
    
    // ...
}

Now, each instance of EnumType is actually a singleton instance of its own class.  Now, your enums are much more powerful:  in addition to fields, each value in the enum can have its own behaviors in the form of concrete implementations of the parent’s abstract method.  There is still no way for another user to add values to the enum—because EnumType’s constructor is private, it can’t be subclassed outside of the class itself.

One minor implementation detail here—the only reason I chose to change the Name property from referencing a field to being abstract and implemented by each subclass was to show that it was an option.  It could just as easily have remained a field of EnumType, and TypeOne and TypeTwo would have called the base construtor with the appropriate strings instead of overriding the Name property.  I am not convinced that one way is better than the other; it’s just a stylistic preference.

We’re making progress away from the C-style need to treat everything as an int, but there’s still a lot of distance to cover.  Notably, we still can’t switch on the enum types (long, chained if-else blocks are so inelegant!), and we've lost the support of methods from System.Enum like GetValues and Parse.  I’ll explore how to resolve these issues and try to think about how C# or a language like it could provide better support for this sort of enum in upcoming posts.