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) Julien Lebosquain has recently 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.


    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.


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

    You can have a real compiler enforced enum constraint by abusing constraint inheritance. The following code specifies both a class and a struct constraints at the same time:

    public abstract class EnumClassUtils<TClass>
    where TClass : class
    {
    
        public static TEnum Parse<TEnum>(string value)
        where TEnum : struct, TClass
        {
            return (TEnum) Enum.Parse(typeof(TEnum), value);
        }
    
    }
    
    public class EnumUtils : EnumClassUtils<Enum>
    {
    }
    

    Usage:

    EnumUtils.Parse<SomeEnum>("value");
    

    Note: this is specifically stated in the C# 5.0 language specification:

    If type parameter S depends on type parameter T then: [...] It is valid for S to have the value type constraint and T to have the reference type constraint. Effectively this limits T to the types System.Object, System.ValueType, System.Enum, and any interface type.

    链接地址: http://www.djcxy.com/p/3576.html

    上一篇: 字节+字节=整数...为什么?

    下一篇: 创建通用方法约束T到一个枚举