Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create Generic method constraining T to an Enum

I'm building a function to extend the Enum.Parse concept that

  • Allows a default value to be parsed in case that an Enum value is not found
  • Is case insensitive

So I wrote the following:

public static T GetEnumFromString<T>(string value, T defaultValue) where T : Enum {     if (string.IsNullOrEmpty(value)) return defaultValue;     foreach (T item in Enum.GetValues(typeof(T)))     {         if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;     }     return defaultValue; } 

I am getting a Error Constraint cannot be special class System.Enum.

Fair enough, but is there a workaround to allow a Generic Enum, or am I going to have to mimic the Parse function and pass a type as an attribute, which forces the ugly boxing requirement to your code.

EDIT All suggestions below have been greatly appreciated, thanks.

Have settled on (I've left the loop to maintain case insensitivity - I am using this when parsing XML)

public static class EnumUtils {     public static T ParseEnum<T>(string value, T defaultValue) where T : struct, IConvertible     {         if (!typeof(T).IsEnum) throw new ArgumentException("T must be an enumerated type");         if (string.IsNullOrEmpty(value)) return defaultValue;          foreach (T item in Enum.GetValues(typeof(T)))         {             if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;         }         return defaultValue;     } } 

EDIT: (16th Feb 2015) Christopher Currens has posted a compiler enforced type-safe generic solution in MSIL or F# below, which is well worth a look, and an upvote. I will remove this edit if the solution bubbles further up the page.

EDIT 2: (13th Apr 2021) As this has now been addressed, and supported, since C# 7.3, I have changed the accepted answer, though full perusal of the top answers is worth it for academic, and historical, interest :)

like image 376
johnc Avatar asked Sep 17 '08 01:09

johnc


People also ask

How do you use generic enums?

The enum is a default subclass of the generic Enum<T> class, where T represents generic enum type. This is the common base class of all Java language enumeration types. The transformation from enum to a class is done by the Java compiler during compilation.

Can you add methods to enums?

Enum Class in JavaAn enum class can include methods and fields just like regular classes. When we create an enum class, the compiler will create instances (objects) of each enum constants.

Can we assign variable to enum?

A change in the default value of an enum member will automatically assign incremental values to the other members sequentially. You can even assign different values to each member. The enum can be of any numeric data type such as byte, sbyte, short, ushort, int, uint, long, or ulong.

Can we extend enum in C#?

Answers. Yes, you can easily define a enumeration extending existing enum.


2 Answers

Since Enum Type implements IConvertible interface, a better implementation should be something like this:

public T GetEnumFromString<T>(string value) where T : struct, IConvertible {    if (!typeof(T).IsEnum)     {       throw new ArgumentException("T must be an enumerated type");    }     //... } 

This will still permit passing of value types implementing IConvertible. The chances are rare though.

like image 194
Vivek Avatar answered Sep 21 '22 19:09

Vivek


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); 
like image 40
Christopher Currens Avatar answered Sep 18 '22 19:09

Christopher Currens