Create Generic Method Constraining T to an Enum

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);

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.

Dart - Constrain a generic type (T) to an Enum

It is not possible in Dart. I had the same issue converting enum properties to a SQLite database (which can hold only its numeric types), so I needed enum.values to "parse" the integer to enum and enum.index to convert the enum value to an int.

The only way possible is to cast the enum to dynamic or passing the enum values.

Example:

T mapToEnum<T>(List<T> values, int value) {
if (value == null) {
return null;
}

return values[value];
}

dynamic enumToMap<T>(List<T> values, T value) {
if (value == null) {
return null;
}

return values.firstWhere((v) => v == value);
}

So I can use like this:

final SomeEnum value = SomeEnum.someValue;
final int intValue = enumToMap(SomeEnum.values, value);
final SomeEnum enumValue = mapToEnum(SomeEnum.values, value.index);

Generic enum as method parameter

Why do you want to pass the enums, while you could pass integers ?

var newObject = new MyObject((int)MyEnum1.Something);
var anotherObject = new MyObject((int)MyEnum2.Dodo);

and use your first constructor :

public MyObject(int id){
ID = id;
}

Generic enum constraint

You could use : Id = Convert.ToInt32(enumValue) instead of the casting (int)

And if you want to add some 'constraint' to check the type at the compilation you could set 'where T : struct' it will at least prevent to set class type.

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.

How to constrain the type of generic parameter to a Typescript function to be an enum

You can constrain the type to a specific enum.

function foo<T extends MyEnum>(param: T) {    
}

However, you cannot constrain it to be any enum (see specs).

In addition to generic interfaces, we can also create generic classes. Note that it is not possible to create generic enums and namespaces.

Can't cast an `Enum` value to `int` in a generic method

enum can be a long or other things

public static void Stuff<T>(T val) where T : System.Enum, IConvertible
{
int v = val.ToInt32(null);
}

This works

If you look at Enum you can see that its not stated as an int.
The actual base classes and implementations are:

public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible

BTW: prefer long over int since longs are used as Flags in enumns to allow 64 flags

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.



Related Topics



Leave a reply



Submit