This has been a pet peeve of mine since I started using .NET but I was curious in case I was missing something. My code snippet won\'t compile (please forgive the forced nat
Heck, the situation is far worse than just dealing with enums. We don't even do this for bools!
public class Test {
public string GetDecision(bool decision) {
switch (decision) {
case true: return "Yes, that's my decision";
case false: return "No, that's my decision";
}
}
}
Produces the same error.
Even if you solved all the problems with enums being able to take on any value, you'd still have this issue. The flow analysis rules of the language simply do not consider switches without defaults to be "exhaustive" of all possible code paths, even when you and I know they are.
I would like very much to fix that, but frankly, we have many higher priorities than fixing this silly little issue, so we've never gotten around to it.
I always think of that default as the fall through/exception.
So here it would not be maybe but instead would be "Invalid Decision, contact support".
I don't see how it would fall through to that but that would be the catchall/exception case.
That's because the value of decision
could actually be a value that is not part of the enumeration, for instance :
string s = GetDecision((Decision)42);
This kind of thing is not prevented by the compiler or the CLR. The value could also be a combination of enum values :
string s = GetDecision(Decision.Yes | Decision.No);
(even if the enum doesn't have the Flags
attribute)
Because of that, you should always put a default
case in you switch, since you can't check all possible values explicitly
public enum Decision { Yes, No}
public class Test
{
public string GetDecision(Decision decision)
{
switch (decision)
{
case Decision.Yes:
return "Yes, that's my decision";
case Decision.No:
return "No, that's my decision";
default: throw new Exception(); // raise exception here.
}
}
}
For the sake of sharing a quirky idea if nothing else, here goes:
...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.
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:
This is what you must do:
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:
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...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.)
In addition to the case of, you can cast any int to your enum and have an enum you aren't handling. There is also the case where, if the enum is in an external .dll, and that .dll is updated, it does not break your code if an additional option is added to the enum (like Yes, No, Maybe). So, to handle those future changes you need the default case as well. There is no way to guarantee at compile time that you know every value that enum will have for it's life.