如何使用TryParse方法解析枚举类型的值?

115
我想编写一个函数,可以将给定值(作为字符串传递)与一个enum的可能值进行验证。如果匹配成功,则应返回枚举实例;否则,应返回默认值。
该函数不得在内部使用try/catch,这排除了使用Enum.Parse,因为它会在接收到无效参数时抛出异常。
我想使用类似于TryParse函数的方法来实现此功能:
public static TEnum ToEnum<TEnum>(this string strEnumValue, TEnum defaultValue)
{
   object enumValue;
   if (!TryParse (typeof (TEnum), strEnumValue, out enumValue))
   {
       return defaultValue;
   }
   return (TEnum) enumValue;
}

8
我不理解这个问题;你的意思是“我想解决这个问题,但是不想使用任何能让我得到答案的方法。”这有什么意义? - Domenic
1
异常处理的成本并不那么高。内部实现所有枚举转换都充满了异常处理。 虽然在正常应用逻辑中抛出和捕获异常有时可能很有用,但我真的不喜欢这样做。当所有异常被抛出时(即使它们被捕获),有时可以将其作为断点。到处抛出异常会使使用起来更加麻烦 :) - Thorarin
4
我只是在寻找比我已知的更好的解决方案。你会去铁路查询问已知的路线或列车吗 :) ? - Manish Basantani
2
@Yogi,@Thorarin: try...catch总是我的最后选择。关于成本高昂,我们从未知道。如果有人在100个项目的列表上调用我的实用方法呢? - Manish Basantani
2
@Amby,仅仅进入try/catch块的成本是微不足道的。抛出异常的成本是显著的,但这应该是例外情况,对吧?此外,不要说“我们永远不知道”……对代码进行分析并找出答案。不要浪费时间想某些东西是否慢,去寻找答案! - akmad
显示剩余2条评论
14个回答

126

Enum.IsDefined可以完成任务。它可能没有TryParse高效,但不需要异常处理也可以工作。

public static TEnum ToEnum<TEnum>(this string strEnumValue, TEnum defaultValue)
{
    if (!Enum.IsDefined(typeof(TEnum), strEnumValue))
        return defaultValue;

    return (TEnum)Enum.Parse(typeof(TEnum), strEnumValue);
}

值得注意的是,在.NET 4.0中添加了一个TryParse方法。


1
到目前为止,这是我看过的最好的答案...没有try/catch,也没有GetNames :) - Thomas Levesque
13
使用Enum.IsDefined的缺点:http://blogs.msdn.com/brada/archive/2003/11/29/50903.aspx - Nader Shirazie
6
同时,IsDefined函数中没有忽略大小写的选项。 - Anthony Johnston
2
@Anthony:如果你想支持大小写不敏感,你需要使用GetNames。在内部,所有这些方法(包括Parse)都使用GetHashEntry,它只会进行一次实际的反射。好消息是,.NET 4.0有一个TryParse,而且还是泛型的 :) - Thorarin
+1 它救了我的一天!我正在将一堆代码从.NET 4回溯到.NET 3.5,而你拯救了我 :) - daitangio
http://msdn.microsoft.com/zh-cn/library/dd783499.aspx (NET 4.0中的Enum.TryParse链接) - Alex from Jitbit

32

正如其他人所说,您必须实现自己的TryParse。Simon Mourier提供了一个完整的实现,可以处理所有情况。

如果您正在使用位域枚举(即标志),您还必须处理类似于"MyEnum.Val1|MyEnum.Val2"这样的字符串,它是两个枚举值的组合。如果您只是使用此字符串调用Enum.IsDefined,它会返回false,尽管Enum.Parse可以正确处理它。

更新

如Lisa和Christian在评论中提到的,C#中的Enum.TryParse在.NET4及以上版本中现已可用。

MSDN文档


也许这不是最吸引人的,但我同意在你的代码迁移到.NET 4之前,这绝对是最好的选择。 - Lisa
1
如下所述,但并不明显:从 .Net 4 开始,Enum.TryParse 可用且无需额外编码即可工作。更多信息可在 MSDN 上找到:http://msdn.microsoft.com/library/vstudio/dd991317%28v=vs.100%29.aspx - Christian
值得注意的是(参考.NET 4.8的源代码),微软实现的Enum.TryParse在某些情况下仍会在内部抛出异常 - 如果字符串以数字或+/-开头,并且无法将整个字符串解析为数字,则它将从Convert.ChangeType中内部捕获一个异常,其他异常可能会从调用ToObject中内部抛出,然后在函数中被捕获,导致返回一个false代码。如果需要真正没有异常的解析器,则Enum.TryParse不能胜任。 - Steve

20
这是一个自定义的EnumTryParse实现,与其他常见的实现不同,它还支持带有Flags属性标记的枚举。
    /// <summary>
    /// Converts the string representation of an enum to its Enum equivalent value. A return value indicates whether the operation succeeded.
    /// This method does not rely on Enum.Parse and therefore will never raise any first or second chance exception.
    /// </summary>
    /// <param name="type">The enum target type. May not be null.</param>
    /// <param name="input">The input text. May be null.</param>
    /// <param name="value">When this method returns, contains Enum equivalent value to the enum contained in input, if the conversion succeeded.</param>
    /// <returns>
    /// true if s was converted successfully; otherwise, false.
    /// </returns>
    public static bool EnumTryParse(Type type, string input, out object value)
    {
        if (type == null)
            throw new ArgumentNullException("type");

        if (!type.IsEnum)
            throw new ArgumentException(null, "type");

        if (input == null)
        {
            value = Activator.CreateInstance(type);
            return false;
        }

        input = input.Trim();
        if (input.Length == 0)
        {
            value = Activator.CreateInstance(type);
            return false;
        }

        string[] names = Enum.GetNames(type);
        if (names.Length == 0)
        {
            value = Activator.CreateInstance(type);
            return false;
        }

        Type underlyingType = Enum.GetUnderlyingType(type);
        Array values = Enum.GetValues(type);
        // some enums like System.CodeDom.MemberAttributes *are* flags but are not declared with Flags...
        if ((!type.IsDefined(typeof(FlagsAttribute), true)) && (input.IndexOfAny(_enumSeperators) < 0))
            return EnumToObject(type, underlyingType, names, values, input, out value);

        // multi value enum
        string[] tokens = input.Split(_enumSeperators, StringSplitOptions.RemoveEmptyEntries);
        if (tokens.Length == 0)
        {
            value = Activator.CreateInstance(type);
            return false;
        }

        ulong ul = 0;
        foreach (string tok in tokens)
        {
            string token = tok.Trim(); // NOTE: we don't consider empty tokens as errors
            if (token.Length == 0)
                continue;

            object tokenValue;
            if (!EnumToObject(type, underlyingType, names, values, token, out tokenValue))
            {
                value = Activator.CreateInstance(type);
                return false;
            }

            ulong tokenUl;
            switch (Convert.GetTypeCode(tokenValue))
            {
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.SByte:
                    tokenUl = (ulong)Convert.ToInt64(tokenValue, CultureInfo.InvariantCulture);
                    break;

                //case TypeCode.Byte:
                //case TypeCode.UInt16:
                //case TypeCode.UInt32:
                //case TypeCode.UInt64:
                default:
                    tokenUl = Convert.ToUInt64(tokenValue, CultureInfo.InvariantCulture);
                    break;
            }

            ul |= tokenUl;
        }
        value = Enum.ToObject(type, ul);
        return true;
    }

    private static char[] _enumSeperators = new char[] { ',', ';', '+', '|', ' ' };

    private static object EnumToObject(Type underlyingType, string input)
    {
        if (underlyingType == typeof(int))
        {
            int s;
            if (int.TryParse(input, out s))
                return s;
        }

        if (underlyingType == typeof(uint))
        {
            uint s;
            if (uint.TryParse(input, out s))
                return s;
        }

        if (underlyingType == typeof(ulong))
        {
            ulong s;
            if (ulong.TryParse(input, out s))
                return s;
        }

        if (underlyingType == typeof(long))
        {
            long s;
            if (long.TryParse(input, out s))
                return s;
        }

        if (underlyingType == typeof(short))
        {
            short s;
            if (short.TryParse(input, out s))
                return s;
        }

        if (underlyingType == typeof(ushort))
        {
            ushort s;
            if (ushort.TryParse(input, out s))
                return s;
        }

        if (underlyingType == typeof(byte))
        {
            byte s;
            if (byte.TryParse(input, out s))
                return s;
        }

        if (underlyingType == typeof(sbyte))
        {
            sbyte s;
            if (sbyte.TryParse(input, out s))
                return s;
        }

        return null;
    }

    private static bool EnumToObject(Type type, Type underlyingType, string[] names, Array values, string input, out object value)
    {
        for (int i = 0; i < names.Length; i++)
        {
            if (string.Compare(names[i], input, StringComparison.OrdinalIgnoreCase) == 0)
            {
                value = values.GetValue(i);
                return true;
            }
        }

        if ((char.IsDigit(input[0]) || (input[0] == '-')) || (input[0] == '+'))
        {
            object obj = EnumToObject(underlyingType, input);
            if (obj == null)
            {
                value = Activator.CreateInstance(type);
                return false;
            }
            value = obj;
            return true;
        }

        value = Activator.CreateInstance(type);
        return false;
    }

1
你提供了最好的实现,我已经用它来满足我的需求;但是,我想知道为什么你使用 Activator.CreateInstance(type) 来创建默认的枚举值而不是 Enum.ToObject(type, 0)。只是个人口味问题吗? - Pierre Arnaud
1
@Pierre - 嗯...不是,那个时间似乎更自然 :-) 也许 Enum.ToObject 更快,因为它在内部使用了一个内部调用 InternalBoxEnum?我从未检查过... - Simon Mourier
2
如下所述,但并不明显:从 .Net 4 开始,Enum.TryParse 可用且无需额外编码即可工作。更多信息可在 MSDN 上找到:http://msdn.microsoft.com/library/vstudio/dd991317%28v=vs.100%29.aspx - Christian

17
最终,您需要在Enum.GetNames周围实现此功能:
public bool TryParseEnum<T>(string str, bool caseSensitive, out T value) where T : struct {
    // Can't make this a type constraint...
    if (!typeof(T).IsEnum) {
        throw new ArgumentException("Type parameter must be an enum");
    }
    var names = Enum.GetNames(typeof(T));
    value = (Enum.GetValues(typeof(T)) as T[])[0];  // For want of a better default
    foreach (var name in names) {
        if (String.Equals(name, str, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) {
            value = (T)Enum.Parse(typeof(T), name);
            return true;
        }
    }
    return false;
}

附加说明:

  • Enum.TryParse 包含在.NET 4中。请参见这里
  • 另一种方法是直接包装 Enum.Parse 并捕获失败时抛出的异常。如果找到匹配项,这可能会更快,但如果没有匹配项,则可能会更慢。根据您正在处理的数据,这可能或可能不是一个净改进。

编辑:刚刚看到了更好的实现方法,该方法缓存了必要的信息:http://damieng.com/blog/2010/10/17/enums-better-syntax-improved-performance-and-tryparse-in-net-3-5


我本打算建议使用 default(T) 来设置默认值。结果发现这对于所有枚举类型都不适用。例如,如果枚举的底层类型是 int,default(T) 将始终返回 0,但这可能是无效的枚举值。 - Daniel Ballinger
Damieng博客上的实现不支持带有“Flags”属性的枚举。 - Uwe Keim

11

基于.NET 4.5

下面是示例代码

using System;

enum Importance
{
    None,
    Low,
    Medium,
    Critical
}

class Program
{
    static void Main()
    {
    // The input value.
    string value = "Medium";

    // An unitialized variable.
    Importance importance;

    // Call Enum.TryParse method.
    if (Enum.TryParse(value, out importance))
    {
        // We now have an enum type.
        Console.WriteLine(importance == Importance.Medium);
    }
    }
}

参考: http://www.dotnetperls.com/enum-parse


6
enum EnumStatus
{
    NAO_INFORMADO = 0,
    ENCONTRADO = 1,
    BLOQUEADA_PELO_ENTREGADOR = 2,
    DISPOSITIVO_DESABILITADO = 3,
    ERRO_INTERNO = 4,
    AGARDANDO = 5
}

...

if (Enum.TryParse<EnumStatus>(item.status, out status)) {

}

我得到了一个错误,状态在当前上下文中不存在。 - Muflix
在我的情况下,这个代码可以正常工作: Enum.TryParse<EnumStatus>(someStatusAsInt.ToString(), out EnumStatus status) - Muflix

4

我有一个优化的实现,你可以在UnconstrainedMelody中使用。实际上,它只是缓存名称列表,但是它以一种漂亮、强类型和通用约束的方式进行缓存 :)


2

目前没有现成的Enum.TryParse方法。这个问题已经在Connect上提出过(仍然没有Enum.TryParse),并得到了回应,可能会在.NET 3.5之后的下一个框架中包含此功能。目前您需要实现建议的解决方法。


1
避免异常处理的唯一方法是使用GetNames()方法,我们都知道异常不应该被滥用于常见应用逻辑 :)

1
这不是唯一的方法。Enum.IsDefined(..)将防止在用户代码中抛出异常。 - Thorarin

1

缓存动态生成的函数/字典是否可行?

因为您(似乎)事先不知道枚举的类型,第一次执行可能会生成后续执行可以利用的内容。

您甚至可以缓存Enum.GetNames()的结果。

您是在优化 CPU 还是内存? 您真的需要这样吗?


优化 CPU 是我的想法。我同意可以以内存为代价来实现它,但这不是我要寻找的解决方案。谢谢。 - Manish Basantani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接