C# 创建将 T 限制为 Enum 的通用方法

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/79126/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 11:17:15  来源:igfitidea点击:

Create Generic method constraining T to an Enum

提问by johnc

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

我正在构建一个函数来扩展这个Enum.Parse概念

  • Allows a default value to be parsed in case that an Enum value is not found
  • Is case insensitive
  • 允许在未找到 Enum 值的情况下解析默认值
  • 不区分大小写

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.

我得到一个错误约束不能是特殊类System.Enum

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

很公平,但是是否有允许通用枚举的解决方法,或者我是否必须模仿该Parse函数并将类型作为属性传递,这会强制对您的代码进行丑陋的装箱要求。

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

已经确定(我已经离开循环以保持不区分大小写 - 我在解析 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.

编辑:(2015 年 2 月 16 日)Julien Lebosquain 最近在下面的MSIL 或 F#中发布了一个编译器强制类型安全的泛型解决方案,非常值得一看,并点赞。如果解决方案在页面上进一步冒泡,我将删除此编辑。

采纳答案by Vivek

Since EnumType implements IConvertibleinterface, a better implementation should be something like this:

由于EnumType 实现了IConvertible接口,所以更好的实现应该是这样的:

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.

这仍将允许传递实现IConvertible. 不过机会很少。

回答by dimarzionist

Hope this is helpful:

希望这是有帮助的:

public static TValue ParseEnum<TValue>(string value, TValue defaultValue)
                  where TValue : struct // enum 
{
      try
      {
            if (String.IsNullOrEmpty(value))
                  return defaultValue;
            return (TValue)Enum.Parse(typeof (TValue), value);
      }
      catch(Exception ex)
      {
            return defaultValue;
      }
}

回答by Bivoauc

I modified the sample by dimarzionist. This version will only work with Enums and not let structs get through.

我修改了 dimarzionist 的样本。此版本仅适用于 Enums 而不会让结构体通过。

public static T ParseEnum<T>(string enumString)
    where T : struct // enum 
    {
    if (String.IsNullOrEmpty(enumString) || !typeof(T).IsEnum)
       throw new Exception("Type given must be an Enum");
    try
    {

       return (T)Enum.Parse(typeof(T), enumString, true);
    }
    catch (Exception ex)
    {
       return default(T);
    }
}

回答by Karg

You can define a static constructor for the class that will check that the type T is an enum and throw an exception if it is not. This is the method mentioned by Jeffery Richter in his book CLR via C#.

您可以为该类定义一个静态构造函数,该构造函数将检查类型 T 是否为枚举,如果不是则抛出异常。这是 Jeffery Richter 在他的 CLR via C# 一书中提到的方法。

internal sealed class GenericTypeThatRequiresAnEnum<T> {
    static GenericTypeThatRequiresAnEnum() {
        if (!typeof(T).IsEnum) {
        throw new ArgumentException("T must be an enumerated type");
        }
    }
}

Then in the parse method, you can just use Enum.Parse(typeof(T), input, true) to convert from string to the enum. The last true parameter is for ignoring case of the input.

然后在 parse 方法中,您可以使用 Enum.Parse(typeof(T), input, true) 将字符串转换为枚举。最后一个 true 参数用于忽略输入的大小写。

回答by Andrew Backer

Interestingly enough, apparently this is possible in other langauges(Managed C++, IL directly).

有趣的是,显然这在其他语言中可能的(Managed C++, IL direct)。

To Quote:

报价:

... Both constraints actually produce valid IL and can also be consumed by C# if written in another language (you can declare those constraints in managed C++ or in IL).

...这两个约束实际上都会产生有效的 IL,如果用另一种语言编写,也可以被 C# 使用(您可以在托管 C++ 或 IL 中声明这些约束)。

Who knows

谁知道

回答by Sunny Rajwadi

I do have specific requirement where I required to use enum with text associated with enum value. For example when I use enum to specify error type it required to describe error details.

我确实有特定要求,我需要将 enum 与与 enum 值关联的文本一起使用。例如,当我使用 enum 指定错误类型时,它需要描述错误详细信息。

public static class XmlEnumExtension
{
    public static string ReadXmlEnumAttribute(this Enum value)
    {
        if (value == null) throw new ArgumentNullException("value");
        var attribs = (XmlEnumAttribute[]) value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof (XmlEnumAttribute), true);
        return attribs.Length > 0 ? attribs[0].Name : value.ToString();
    }

    public static T ParseXmlEnumAttribute<T>(this string str)
    {
        foreach (T item in Enum.GetValues(typeof(T)))
        {
            var attribs = (XmlEnumAttribute[])item.GetType().GetField(item.ToString()).GetCustomAttributes(typeof(XmlEnumAttribute), true);
            if(attribs.Length > 0 && attribs[0].Name.Equals(str)) return item;
        }
        return (T)Enum.Parse(typeof(T), str, true);
    }
}

public enum MyEnum
{
    [XmlEnum("First Value")]
    One,
    [XmlEnum("Second Value")]
    Two,
    Three
}

 static void Main()
 {
    // Parsing from XmlEnum attribute
    var str = "Second Value";
    var me = str.ParseXmlEnumAttribute<MyEnum>();
    System.Console.WriteLine(me.ReadXmlEnumAttribute());
    // Parsing without XmlEnum
    str = "Three";
    me = str.ParseXmlEnumAttribute<MyEnum>();
    System.Console.WriteLine(me.ReadXmlEnumAttribute());
    me = MyEnum.One;
    System.Console.WriteLine(me.ReadXmlEnumAttribute());
}

回答by Jeff

I always liked this (you could modify as appropriate):

我一直很喜欢这个(你可以适当修改):

public static IEnumerable<TEnum> GetEnumValues()
{
  Type enumType = typeof(TEnum);

  if(!enumType.IsEnum)
    throw new ArgumentException("Type argument must be Enum type");

  Array enumValues = Enum.GetValues(enumType);
  return enumValues.Cast<TEnum>();
}

回答by Martin

I tried to improve the code a bit:

我试着稍微改进一下代码:

public T LoadEnum<T>(string value, T defaultValue = default(T)) where T : struct, IComparable, IFormattable, IConvertible
{
    if (Enum.IsDefined(typeof(T), value))
    {
        return (T)Enum.Parse(typeof(T), value, true);
    }
    return defaultValue;
}

回答by Christopher Currens

This feature is finally supported in C# 7.3!

C# 7.3 终于支持这个特性了!

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

以下代码段(来自dotnet 示例)演示了如何:

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.

请务必将 C# 项目中的语言版本设置为 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 possiblein MSIL. I wrote this little....thing

我迟到了,但我把它当作一个挑战,看看它是如何完成的。这在 C#(或 VB.NET,但向下滚动 F#)中是不可能的,但在 MSIL 中是可能的。我写了这个小....东西

// 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 wouldlook like this, if it were valid C#:

如果它是有效的 C#,它生成一个看起来像这样的函数:

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

Then with the following C# code:

然后使用以下 C# 代码:

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.

不幸的是,这意味着这部分代码是用 MSIL 而不是 C# 编写的,唯一增加的好处是您可以通过System.Enum. 它也是一种无赖,因为它被编译成一个单独的程序集。但是,这并不意味着您必须以这种方式部署它。

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

通过删除该行.assembly MyThing{}并调用 ilasm 如下:

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 toopainful 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.

不幸的是,VS2010(显然更早版本)不支持添加网络模块引用,这意味着您在调试时必须将其保留在 2 个单独的程序集中。您可以将它们添加为程序集的唯一方法是使用/addmodule:{files}命令行参数自己运行 csc.exe 。在 MSBuild 脚本中不会痛苦。当然,如果你勇敢或愚蠢,你可以每次手动运行csc。而且它肯定会变得更加复杂,因为多个程序集需要访问它。

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.

所以,它可以在 .Net 中完成。值得付出额外的努力吗?嗯,我想我会让你决定那个。



F# Solution as alternative

F# 解决方案作为替代

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

额外的功劳:事实证明,enum除了 MSIL:F# 之外,至少还有一种其他 .NET 语言可以对 进行通用限制。

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 isvery different) and it relies on the FSharp.Corelibrary, which, just like any other external library, needs to become part of your distribution.

这个更容易维护,因为它是一种众所周知的语言,具有完整的 Visual Studio IDE 支持,但您的解决方案中仍然需要一个单独的项目。然而,它自然会产生很大的不同IL(代码非常不同的),它依赖于FSharp.Core库,它,就像任何其他外部库,需要成为你的发行版的一部分。

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:

以下是如何使用它(与 MSIL 解决方案基本相同),并表明它在其他同义结构上正确失败:

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

回答by expert

I loved Christopher Currens's solution using IL but for those who don't want to deal with tricky business of including MSIL into their build process I wrote similar function in C#.

我喜欢 Christopher Currens 使用 IL 的解决方案,但对于那些不想处理将 MSIL 包含到他们的构建过程中的棘手业务的人,我在 C# 中编写了类似的函数。

Please note though that you can't use generic restriction like where T : Enumbecause Enum is special type. Therefore I have to check if given generic type is really enum.

请注意,您不能使用泛型限制,where T : Enum因为 Enum 是特殊类型。因此我必须检查给定的泛型类型是否真的是枚举。

My function is:

我的功能是:

public static T GetEnumFromString<T>(string strValue, T defaultValue)
{
    // Check if it realy enum at runtime 
    if (!typeof(T).IsEnum)
        throw new ArgumentException("Method GetEnumFromString can be used with enums only");

    if (!string.IsNullOrEmpty(strValue))
    {
        IEnumerator enumerator = Enum.GetValues(typeof(T)).GetEnumerator();
        while (enumerator.MoveNext())
        {
            T temp = (T)enumerator.Current;
            if (temp.ToString().ToLower().Equals(strValue.Trim().ToLower()))
                return temp;
        }
    }

    return defaultValue;
}