For the sake of sharing a quirky idea if nothing else, here goes:
You can always implement your own strong enums
...and since the introduction of the nameof
operator you can also use them in switch-cases.
(Not that you couldn't technically do so previously, but it was difficult to make such code readable and refactor friendly.)
public struct MyEnum : IEquatable<MyEnum>
{
private readonly string name;
private MyEnum(string name) { name = name; }
public string Name
{
// ensure observable pureness and true valuetype behavior of our enum
get { return name ?? nameof(Bork); } // <- by choosing a default here.
}
// our enum values:
public static readonly MyEnum Bork;
public static readonly MyEnum Foo;
public static readonly MyEnum Bar;
public static readonly MyEnum Bas;
// automatic initialization:
static MyEnum()
{
FieldInfo[] values = typeof(MyEnum).GetFields(BindingFlags.Static | BindingFlags.Public);
foreach (var value in values)
value.SetValue(null, new MyEnum(value.Name));
}
/* don't forget these: */
public override bool Equals(object obj)
{
return obj is MyEnum && Equals((MyEnum)obj);
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
public override string ToString()
{
return Name.ToString();
}
public bool Equals(MyEnum other)
{
return Name.Equals(other.Name);
}
public static bool operator ==(MyEnum left, MyEnum right)
{
return left.Equals(right);
}
public static bool operator !=(MyEnum left, MyEnum right)
{
return !left.Equals(right);
}
}
and use it thusly:
public int Example(MyEnum value)
{
switch(value.Name)
{
default: //case nameof(MyEnum.Bork):
return 0;
case nameof(MyEnum.Foo):
return 1;
case nameof(MyEnum.Bar):
return 2;
case nameof(MyEnum.Bas):
return 3;
}
}
and you would of course call that method like so:
int test = Example(MyEnum.Bar); // returns 2
That we can now easily get the Name is basically just a bonus, and yeah some readers might point out that this is basically a Java enum without the null-case (since it's not a class). And just like in Java you can add whatever extra data and or properties you desire to it, e.g. an ordinal value.
Readability: Check!
Intellisense: Check!
Refactorability: Check!
Is a ValueType: Check!
True enumeration: Check!
...
Is it performant? Compared to native enums; no.
Should you use this? Hmmm....
How important is it for you to have true enumerations so you can getting rid of enum runtime checks and their accompanying exceptions?
I don't know. Can't really answer that for you dear reader; to each their own.
...Actually, as I wrote this I realized it would probably be cleaner to let the struct "wrap" a normal enum. (The static struct fields and the corresponding normal enum mirroring each other with the help of similar reflection as above.) Just never use the normal enum as a parameter and you're good.
UPDATE :
Yepp, spent the night testing out my ideas, and I was right: I now have near perfect java-style enums in c#. Usage is clean and performance is improved.
Best of all: all the nasty shit is encapsulated in the base-class, your own concrete implementation can be as clean as this:
// example java-style enum:
public sealed class Example : Enumeration<Example, Example.Const>, IEnumerationMarker
{
private Example () {}
/// <summary> Declare your enum constants here - and document them. </summary>
public static readonly Example Foo = new Example ();
public static readonly Example Bar = new Example ();
public static readonly Example Bas = new Example ();
// mirror your declaration here:
public enum Const
{
Foo,
Bar,
Bas,
}
}
This is what you can do:
- You can add any private fields you want.
- You can add any public non-static fields you want.
- You can add any properties and methods you want.
- You can design your constructors however you wish, because:
- You can forget base constructor hassle. Base constructor is parameter-less!
This is what you must do:
- Your enum must be a sealed class.
- All your constructors must be private.
- Your enum must inherit directly from Enumeration<T, U> and inherit the empty IEnumerationMarker interface.
- The first generic type parameter to Enumeration<T, U> must be your enum class.
- For each public static field there must exist an identically named value in the System.Enum (that you specified as the second generic type parameter to Enumeration<T, U>).
- All your public static fields must be readonly and of your enum type.
- All your public static fields must be assigned a unique non-null value during type initialization.
At the moment every invariant above is asserted at type initialization. Might try to tweak it later to see if some of it can be detected at compile-time.
Requirements Rationale:
- Your enum must be sealed because if it isn't then other invariants become a lot more complicated for no obvious benefit.
- Allowing public constructors makes no sense. It's an enumeration type, which is basically a singleton type but with a fixed set of instances instead of just one.
- Same reason as the first one. The reflection and some of the other invariant and constraint checking just becomes messy if it isn't.
- We need this generic type parameter so the type-system can be used to uniquely store our enum data with performant compile/Jit time bindings. No hash-tables or other slow mechanisms are used! In theory it can be removed, but I don't think it's worth the added cost in complexity and performance to do so.
- This one should be quite obvious. We need these constants for making elegant switch-statements. Of course I could make a second enum-type that doesn't have them; you would still be able to switch using the
nameof
method shown earlier. It just would not be as performant. I'm still contemplating if a should relax this requirement or not. I'll experiment on it...
- Your enum constants must be public and static for obvious reasons, and readonly fields because a) having readonly enumeration instances means all equality checks simplifies to reference equality; b) properties are more flexible and verbose, and frankly both of those are undesirable properties for an enumeration implementation. Lastly all public static fields must be of your enum-type simply because; a) it keeps your enumeration type cleaner with less clutter; b) makes the reflection simpler; and c) you are free to do whatever with properties anyway so it's a very soft restriction.
- This is because I'm trying hard to keep "nasty reflection magic" to a minimum. I don't want my enum implementation to require full-trust execution. That would severely limit its usefulness. More precisely, calling private constructors or writing to readonly fields can throw security exceptions in a low-trust environment. Thus your enum must instantiate your enum constants at initialization time itself - then we can populate the (internal) base-class data of those instances "cleanly".
So anyway, how can you use these java-style enums?
Well I implemented this stuff for now:
int ordinal = Example.Bar.Ordinal; // will be in range: [0, Count-1]
string name = Example.Bas.Name; // "Bas"
int count = Enumeration.Count<Example>(); // 3
var value = Example.Foo.Value; // <-- Example.Const.Foo
Example[] values;
Enumeration.Values(out values);
foreach (var value in Enumeration.Values<Example>())
Console.WriteLine(value); // "Foo", "Bar", "Bas"
public int Switching(Example value)
{
if (value == null)
return -1;
// since we are switching on a System.Enum tabbing to autocomplete all cases works!
switch (value.Value)
{
case Example.Const.Foo:
return 12345;
case Example.Const.Bar:
return value.GetHasCode();
case Example.Const.Bas:
return value.Ordinal * 42;
default:
return 0;
}
}
The abstract Enumeration class will also implement the IEquatable<Example>
interface for us, including == and != operators that will work on Example instances.
Aside from the reflection needed during type initialization everything is clean and performant. Will probably move on to implement the specialized collections java has for enums.
So where is this code then?
I want to see if I can clean it up a bit more before I release it, but it will probably be up on a dev branch on GitHub by the end of the week - unless I find other crazy projects to work on! ^_^
Now up on GitHub
See Enumeration.cs
and Enumeration_T2.cs
.
They are currently part of the dev branch of a very much wip library I'm working on.
(Nothing is "releasable" yet and subject to breaking changes at any moment.)
...For now the rest of the library is mostly a shit ton of boilerplate to extend all array methods to multi-rank arrays, make multi-rank arrays usable with Linq, and performant ReadOnlyArray wrappers (immutable structs) for exposing (private) arrays in a safe way without the cringy need to create copies all the time.
Everything* except the very latest dev commits is fully documented and IntelliSense friendly.
(*The java enum types are still wip and will be properly documented once I've finalized their design.)