Como TryParse para valor Enum?

94

Quero escrever uma função que pode validar um determinado valor (passado como uma string) em relação aos valores possíveis de um enum. No caso de uma correspondência, ele deve retornar a instância de enum; caso contrário, ele deve retornar um valor padrão.

A função não pode usar try/ internamente catch, o que exclui o uso Enum.Parse, que lança uma exceção quando fornecido um argumento inválido.

Eu gostaria de usar algo semelhante a uma TryParsefunção para implementar isso:

public static TEnum ToEnum<TEnum>(this string strEnumValue, TEnum defaultValue)
{
   object enumValue;
   if (!TryParse (typeof (TEnum), strEnumValue, out enumValue))
   {
       return defaultValue;
   }
   return (TEnum) enumValue;
}
Manish Basantani
fonte
8
Eu não entendo essa pergunta; você está dizendo "Quero resolver este problema, mas não quero usar nenhum dos métodos que me dariam uma solução." Qual é o ponto?
Domenic
1
Qual é a sua aversão em tentar / pegar uma solução? Se você está tentando evitar exceções porque elas são "caras", dê um tempo. Em 99% dos casos, a exceção de custo de custo para lançar / capturar é insignificante em comparação com seu código principal.
SolutionYogi
1
O custo do tratamento de exceções não é tão ruim. Inferno, as implementações internas de toda essa conversão de enumeração estão cheias de manipulação de exceção. Eu realmente não gosto de exceções lançadas e capturadas durante a lógica normal do aplicativo. Às vezes, pode ser útil interromper todas as exceções lançadas (mesmo quando detectadas). Lançar exceções por todo o lugar tornará isso muito mais chato de usar :)
Thorarin
3
@Domenic: Estou apenas procurando uma solução melhor do que a que já conheço. Você iria a um inquérito ferroviário para pedir uma rota ou trem que você já conhece :).
Manish Basantani
2
@Amby, o custo de simplesmente inserir um bloco try / catch é insignificante. O custo de JOGAR uma exceção não é, mas isso deveria ser excepcional, não? Além disso, não diga "nunca sabemos" ... analise o código e descubra. Não perca seu tempo se perguntando se algo está lento, DESCUBRA!
akmad

Respostas:

31

Como outros já disseram, você deve implementar o seu próprio TryParse. Simon Mourier está fornecendo uma implementação completa que cuida de tudo.

Se você estiver usando enums de campo de bits (isto é, sinalizadores), você também terá que lidar com uma string como a "MyEnum.Val1|MyEnum.Val2"qual é uma combinação de dois valores de enum. Se você apenas chamar Enum.IsDefinedcom esta string, ele retornará false, embora Enum.Parseseja correto.

Atualizar

Conforme mencionado por Lisa e Christian nos comentários, Enum.TryParseagora está disponível para C # em .NET4 e superior.

MSDN Docs

Victor Arndt Mueller
fonte
Talvez o menos atraente, mas concordo que este é definitivamente o melhor até que seu código seja migrado para .NET 4.
Lisa
1
Conforme mencionado abaixo, mas não realmente visível: A partir de .Net 4 Enum.ExperimenteParse está disponível e funciona sem codificação extra. Mais informações estão disponíveis no MSDN: msdn.microsoft.com/library/vstudio/dd991317%28v=vs.100%29.aspx
Christian,
106

Enum.IsDefined fará as coisas. Pode não ser tão eficiente quanto um TryParse provavelmente seria, mas funcionará sem tratamento de exceção.

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

Vale a pena notar: um TryParsemétodo foi adicionado no .NET 4.0.

Thorarin
fonte
1
Melhor resposta que já vi até agora ... sem tentar / pegar, sem GetNames :)
Thomas Levesque
13
Desvantagens de Enum.IsDefined: blogs.msdn.com/brada/archive/2003/11/29/50903.aspx
Nader Shirazie
6
também não há nenhum caso para ignorar em IsDefined
Anthony Johnston
2
@Anthony: se você deseja apoiar a não diferenciação de maiúsculas e minúsculas, você precisa GetNames. Internamente, todos esses métodos (incluindo Parse) usam GetHashEntry, que faz a reflexão real - uma vez. O lado bom é que o .NET 4.0 tem um TryParse e é genérico também :)
Thorarin
+1 Isso salvou meu dia! Estou fazendo backport de um monte de código do .NET 4 para o .NET 3.5 e você me salvou :)
daitangio
20

Aqui está uma implementação personalizada de EnumTryParse. Ao contrário de outras implementações comuns, ele também suporta enum marcado com o Flagsatributo.

    /// <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;
    }
Simon Mourier
fonte
1
você forneceu a melhor implementação e eu a usei para meus próprios fins; no entanto, estou me perguntando por que você usa Activator.CreateInstance(type)para criar o valor enum padrão e não Enum.ToObject(type, 0). É só uma questão de gosto?
Pierre Arnaud,
1
@Pierre - Hmmm ... não, parecia mais natural naquele momento :-) Talvez Enum.ToObject seja mais rápido, pois está usando internamente uma chamada interna InternalBoxEnum? Eu nunca verifiquei isso ...
Simon Mourier
2
Conforme mencionado abaixo, mas não realmente visível: A partir de .Net 4 Enum.ExperimenteParse está disponível e funciona sem codificação extra. Mais informações estão disponíveis no MSDN: msdn.microsoft.com/library/vstudio/dd991317%28v=vs.100%29.aspx
Christian,
16

No final, você deve implementar isso em torno de 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;
}

Notas Adicionais:

  • Enum.TryParseestá incluído no .NET 4. Veja aqui http://msdn.microsoft.com/library/dd991876(VS.100).aspx
  • Outra abordagem seria empacotar diretamente a Enum.Parsecaptura da exceção lançada quando ela falha. Isso pode ser mais rápido quando uma correspondência é encontrada, mas provavelmente será mais lento se não for. Dependendo dos dados que você está processando, isso pode ou não ser uma melhoria líquida.

EDIT: Acabei de ver uma implementação melhor sobre isso, que armazena em cache as informações necessárias: http://damieng.com/blog/2010/10/17/enums-better-syntax-improved-performance-and-tryparse-in-net- 3-5

Richard
fonte
Eu ia sugerir o uso de default (T) para definir o valor padrão. Acontece que isso não funcionaria para todos os enums. Por exemplo, se o tipo subjacente para o enum era int, o padrão (T) sempre retornará 0, que pode ou não ser válido para o enum.
Daniel Ballinger
A implementação no blog de Damieng não suporta enums com o Flagsatributo.
Uwe Keim
9

Baseado em .NET 4.5

Amostra de código abaixo

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

Referência: http://www.dotnetperls.com/enum-parse

Hugo Hilário
fonte
4

Eu tenho uma implementação otimizada que você pode usar no UnconstrainedMelody . Efetivamente, ele está apenas armazenando em cache a lista de nomes, mas está fazendo isso de uma forma agradável, fortemente tipada e genericamente restrita :)

Jon Skeet
fonte
4
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)) {

}
Everson Rafael
fonte
2

Atualmente não há Enum.ExperimenteParse pronto para uso. Isso foi solicitado no Connect ( Still no Enum.TryParse ) e obteve uma resposta indicando possível inclusão na próxima estrutura após .NET 3.5. Você terá que implementar as soluções alternativas sugeridas por enquanto.

Ahmad Mageed
fonte
1

A única maneira de evitar o tratamento de exceções é usar o método GetNames (), e todos sabemos que as exceções não devem ser abusadas para a lógica de aplicativo comum :)

Philippe Leybaert
fonte
1
Não é a única maneira. Enum.IsDefined (..) impedirá que exceções sejam lançadas no código do usuário.
Thorarin
1

O armazenamento em cache de uma função / dicionário gerado dinamicamente é permitido?

Como você (parece) não sabe o tipo de enum com antecedência, a primeira execução pode gerar algo de que as execuções subsequentes podem tirar proveito.

Você pode até armazenar em cache o resultado de Enum.GetNames ()

Você está tentando otimizar para CPU ou memória? Você realmente precisa?

Nader Shirazie
fonte
A ideia é otimizar a CPU. Concordo que posso fazê-lo com memória de custo. Mas não é a solução que procuro. Obrigado.
Manish Basantani
0

Como outros já disseram, se você não usa Try & Catch, você precisa usar IsDefined ou GetNames ... Aqui estão alguns exemplos ... eles são basicamente todos iguais, o primeiro lidando com enums anuláveis. Eu prefiro o segundo porque é uma extensão das cordas, não enums ... mas você pode mixá-los como quiser!

  • www.objectreference.net/post/Enum-ExperimenteParse-Extension-Method.aspx
  • flatlinerdoa.spaces.live.com/blog/cns!17124D03A9A052B0!605.entry
  • mironabramson.com/blog/post/2008/03/Another-version-for-the-missing-method-EnumExperimenteParse.aspx
  • lazyloading.blogspot.com/2008/04/enumtryparse-with-net-35-extension.html

fonte
0

Não há um TryParse porque o tipo do Enum não é conhecido até o tempo de execução. Um TryParse que segue a mesma metodologia, digamos, do método Date.ExperimenteParse lançaria um erro de conversão implícito no parâmetro ByRef.

Eu sugiro fazer algo assim:

//1 line call to get value
MyEnums enumValue = (Sections)EnumValue(typeof(Sections), myEnumTextValue, MyEnums.SomeEnumDefault);

//Put this somewhere where you can reuse
public static object EnumValue(System.Type enumType, string value, object NotDefinedReplacement)
{
    if (Enum.IsDefined(enumType, value)) {
        return Enum.Parse(enumType, value);
    } else {
        return Enum.Parse(enumType, NotDefinedReplacement);
    }
}
ben
fonte
Para Trymétodos cujos resultados podem ser tipos de valor, ou onde nullpode ser um resultado legítimo (por exemplo, Dictionary.TryGetValue, which has both such traits), the normal pattern is for a método Try` para retornar boole passar o resultado como um outparâmetro. Para aqueles que retornam tipos de classe onde nullnão é um resultado válido, não há dificuldade em usar um nullretorno para indicar falha.
supercat
-1

Dê uma olhada na própria classe Enum (struct?). Existe um método Parse nisso, mas não tenho certeza sobre um tryparse.

Spence
fonte
Eu sei sobre o método Enum.Parse (typeof (TEnum), strEnumValue). Ele lança ArgumentException se strEnumValue não for válido. Procurando TryParse ........
Manish Basantani
-2

Este método converterá um tipo de enum:

  public static TEnum ToEnum<TEnum>(object EnumValue, TEnum defaultValue)
    {
        if (!Enum.IsDefined(typeof(TEnum), EnumValue))
        {
            Type enumType = Enum.GetUnderlyingType(typeof(TEnum));
            if ( EnumValue.GetType() == enumType )
            {
                string name = Enum.GetName(typeof(HLink.ViewModels.ClaimHeaderViewModel.ClaimStatus), EnumValue);
                if( name != null)
                    return (TEnum)Enum.Parse(typeof(TEnum), name);
                return defaultValue;
            }
        }
        return (TEnum)Enum.Parse(typeof(TEnum), EnumValue.ToString());
    } 

Ele verifica o tipo subjacente e obtém o nome dele para analisar. Se tudo falhar, ele retornará o valor padrão.

Naveed Ahmed
fonte
3
o que isso está fazendo "Enum.GetName (typeof (HLink.ViewModels.ClaimHeaderViewModel.ClaimStatus), EnumValue)" Provavelmente alguma dependência de seu código local.
Manish Basantani