Enum Type Constraints in C#

Enum type constraints in C#

This is an occasionally requested feature.

As I'm fond of pointing out, ALL features are unimplemented until someone designs, specs, implements, tests, documents and ships the feature. So far, no one has done that for this one. There's no particularly unusual reason why not; we have lots of other things to do, limited budgets, and this one has never made it past the "wouldn't this be nice?" discussion in the language design team.

The CLR doesn't support it, so in order to make it work we'd need to do runtime work in addition to the language work. (see answer comments)

I can see that there are a few decent usage cases, but none of them are so compelling that we'd do this work rather than one of the hundreds of other features that are much more frequently requested, or have more compelling and farther-reaching usage cases. (If we're going to muck with this code, I'd personally prioritize delegate constraints way, way above enum constraints.)

Create Generic method constraining T to an Enum


This feature is finally supported in C# 7.3!

The following snippet (from the dotnet samples) demonstrates how:

public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
var result = new Dictionary<int, string>();
var values = Enum.GetValues(typeof(T));

foreach (int item in values)
result.Add(item, Enum.GetName(typeof(T), item));
return result;
}

Be sure to set your language version in your C# project to version 7.3.


Original Answer below:

I'm late to the game, but I took it as a challenge to see how it could be done. It's not possible in C# (or VB.NET, but scroll down for F#), but is possible in MSIL. I wrote this little....thing

// license: http://www.apache.org/licenses/LICENSE-2.0.html
.assembly MyThing{}
.class public abstract sealed MyThing.Thing
extends [mscorlib]System.Object
{
.method public static !!T GetEnumFromString<valuetype .ctor ([mscorlib]System.Enum) T>(string strValue,
!!T defaultValue) cil managed
{
.maxstack 2
.locals init ([0] !!T temp,
[1] !!T return_value,
[2] class [mscorlib]System.Collections.IEnumerator enumerator,
[3] class [mscorlib]System.IDisposable disposer)
// if(string.IsNullOrEmpty(strValue)) return defaultValue;
ldarg strValue
call bool [mscorlib]System.String::IsNullOrEmpty(string)
brfalse.s HASVALUE
br RETURNDEF // return default it empty

// foreach (T item in Enum.GetValues(typeof(T)))
HASVALUE:
// Enum.GetValues.GetEnumerator()
ldtoken !!T
call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type)
callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Array::GetEnumerator()
stloc enumerator
.try
{
CONDITION:
ldloc enumerator
callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
brfalse.s LEAVE

STATEMENTS:
// T item = (T)Enumerator.Current
ldloc enumerator
callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
unbox.any !!T
stloc temp
ldloca.s temp
constrained. !!T

// if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;
callvirt instance string [mscorlib]System.Object::ToString()
callvirt instance string [mscorlib]System.String::ToLower()
ldarg strValue
callvirt instance string [mscorlib]System.String::Trim()
callvirt instance string [mscorlib]System.String::ToLower()
callvirt instance bool [mscorlib]System.String::Equals(string)
brfalse.s CONDITION
ldloc temp
stloc return_value
leave.s RETURNVAL

LEAVE:
leave.s RETURNDEF
}
finally
{
// ArrayList's Enumerator may or may not inherit from IDisposable
ldloc enumerator
isinst [mscorlib]System.IDisposable
stloc.s disposer
ldloc.s disposer
ldnull
ceq
brtrue.s LEAVEFINALLY
ldloc.s disposer
callvirt instance void [mscorlib]System.IDisposable::Dispose()
LEAVEFINALLY:
endfinally
}

RETURNDEF:
ldarg defaultValue
stloc return_value

RETURNVAL:
ldloc return_value
ret
}
}

Which generates a function that would look like this, if it were valid C#:

T GetEnumFromString<T>(string valueString, T defaultValue) where T : Enum

Then with the following C# code:

using MyThing;
// stuff...
private enum MyEnum { Yes, No, Okay }
static void Main(string[] args)
{
Thing.GetEnumFromString("No", MyEnum.Yes); // returns MyEnum.No
Thing.GetEnumFromString("Invalid", MyEnum.Okay); // returns MyEnum.Okay
Thing.GetEnumFromString("AnotherInvalid", 0); // compiler error, not an Enum
}

Unfortunately, this means having this part of your code written in MSIL instead of C#, with the only added benefit being that you're able to constrain this method by System.Enum. It's also kind of a bummer, because it gets compiled into a separate assembly. However, it doesn't mean you have to deploy it that way.

By removing the line .assembly MyThing{} and invoking ilasm as follows:

ilasm.exe /DLL /OUTPUT=MyThing.netmodule

you get a netmodule instead of an assembly.

Unfortunately, VS2010 (and earlier, obviously) does not support adding netmodule references, which means you'd have to leave it in 2 separate assemblies when you're debugging. The only way you can add them as part of your assembly would be to run csc.exe yourself using the /addmodule:{files} command line argument. It wouldn't be too painful in an MSBuild script. Of course, if you're brave or stupid, you can run csc yourself manually each time. And it certainly gets more complicated as multiple assemblies need access to it.

So, it CAN be done in .Net. Is it worth the extra effort? Um, well, I guess I'll let you decide on that one.



F# Solution as alternative

Extra Credit: It turns out that a generic restriction on enum is possible in at least one other .NET language besides MSIL: F#.

type MyThing =
static member GetEnumFromString<'T when 'T :> Enum> str defaultValue: 'T =
/// protect for null (only required in interop with C#)
let str = if isNull str then String.Empty else str

Enum.GetValues(typedefof<'T>)
|> Seq.cast<_>
|> Seq.tryFind(fun v -> String.Compare(v.ToString(), str.Trim(), true) = 0)
|> function Some x -> x | None -> defaultValue

This one is easier to maintain since it's a well-known language with full Visual Studio IDE support, but you still need a separate project in your solution for it. However, it naturally produces considerably different IL (the code is very different) and it relies on the FSharp.Core library, which, just like any other external library, needs to become part of your distribution.

Here's how you can use it (basically the same as the MSIL solution), and to show that it correctly fails on otherwise synonymous structs:

// works, result is inferred to have type StringComparison
var result = MyThing.GetEnumFromString("OrdinalIgnoreCase", StringComparison.Ordinal);
// type restriction is recognized by C#, this fails at compile time
var result = MyThing.GetEnumFromString("OrdinalIgnoreCase", 42);

Why is a generic type constrained by 'Enum' failing to qualify as a 'struct' in C# 7.3?

This issue is strange (arguably), but expected, behavior.

The class System.Enum itself could be supplied as the type of T. Being a class, System.Enum is of course not a struct!

public class MCVE<T> where T : Enum { }
public class MCVE2 : MCVE<Enum> { }

As explained by contributor HaloFour:

This is an odd behavior by the CLR itself. System.Enum is a class, but
every type that derives from System.Enum is a struct. So a constraint
on System.Enum by itself doesn't imply struct since you could pass
System.Enum as the generic type argument...

It is weird, but it was easier to simply remove the imposed limitation
on the compiler than to argue over different syntax for "enum"
constraints that might have different behavior.

The solution is to make it your standard practice to constrain to struct, Enum when you wish to constrain concrete types to being any specific enumeration. If additionally you wish to accept the class System.Enum as your generic type, only then would you constrain to Enum.

C# 7.3 Enum constraint: Why can't I use the enum keyword?

The struct constraint on generics doesn't map to an actual type (though it could, in theory, map to ValueType). Similarly, enum doesn't cleanly map to actual types the way string, int, or long do, it sets up special syntax for creating a class of symbolic constants that map to integer values; hence public enum Stuff instead of public class Stuff : Enum. Note that had the latter been implemented instead, it would be more subtle since it would change syntax based on inherited type, instead of changing syntax based on a non-class keyword.

So, in conclusion, yes, where T : enum is not meant to work because enum is a keyword, not a type alias. If you really want to see it work because enum at least smells like a type alias in context like these, go request it!

EDIT: For some historical reference, here's a question from 2008 indicating that Enum was not a valid constraint, since it's a special class.

Using enum as generic type parameter in C#

No, it's not possible unfortunately. The best you can do is use where T : struct, IComparable, IConvertible, IFormattable (which of course is not the same). The interface restrictions are derived from the implementation of System.Enum.

Apart from that, you can check if typeof(T).IsEnum, which can detect the problem at runtime and presumably throw an exception. But there is no way to enforce this restriction at compile time.

System.Enum as a generic type parameter with constraints

As Eric Lippert says that and I quote

ALL features are unimplemented until someone designs, specs, implements, tests, documents and ships the feature. So far, no one has done that for this one. There's no particularly unusual reason why not; we have lots of other things to do, limited budgets, and this one has never made it past the "wouldn't this be nice?" discussion in the language design team."

C# 7.3 Enum constraint: Why can't I use the nullable enum?

You can, but you have to add another constraint: the struct constraint.

public static void DoSomething<T>(T? defaultValue) where T : struct, Enum
{
}

Extension Method on enum parameter must be constrained to a struct

The Problem why T : class won't work is because an enum always is a struct. So you see it the wrong way. You write:

Enum.Result must be a reference type... I thought a class was a
reference type...

You are right that a class is a reference type. But Enum.Result isn't a class. It's a struct as mentioned above. Your constraint T : class just accept a reference type.

Further you can't type var a = "hello world"; in your function if you change constraint to T : enum, because this constraint isn't valid. So you wouldn't be able to write any valid code in your method before you fix your constraint.

Take a look at msdn for clarify which constraints are possible.

Possible bug in C# 7.3 handling of generic Enum constraints

You need an extra constraint:

public class Test<T> where T: struct, Enum
{
public void Method()
{
if (!Enum.TryParse<T>("something", out var value))
throw new Exception("Oops");
}
}

With just where T : Enum, you're allowed to call new Test<Enum>().Method(); -- i.e. pass in the Enum type, rather than any specific type of enum. Adding struct means you have to pass in a specific type of enum.

More specifically, Enum.TryParse<T> has the constraint where T : struct, so you need to match this constraint in your method.



Related Topics



Leave a reply



Submit