小编典典

创建将 T 限制为枚举的通用方法

all

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

  • 允许在未找到 Enum 值的情况下解析默认值
  • 不区分大小写

所以我写了以下内容:

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

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

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

编辑 以下所有建议都非常感谢,谢谢。

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

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

编辑 2: (2021 年 4 月 13 日)由于这个问题现在已经得到解决和支持,自 C# 7.3
以来,我已经更改了接受的答案,尽管全面阅读顶级答案对于学术和历史兴趣是值得的 :)


阅读 314

收藏
2022-02-25

共1个答案

小编典典

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

以下片段(来自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;
}

确保将 C# 项目中的语言版本设置为 7.3 版。


原答案如下:

我玩游戏迟到了,但我把它当作一个挑战,看看它是如何完成的。这在 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
  }
}

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

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

然后使用以下 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
}

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

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

ilasm.exe /DLL /OUTPUT=MyThing.netmodule

你得到一个网络模块而不是一个程序集。

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

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


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

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

以下是如何使用它(基本上与 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);
2022-02-25