Pourquoi le cast int en une valeur d'énumération invalide ne lève-t-il PAS une exception?

124

Si j'ai une énumération comme celle-ci:

enum Beer
{
    Bud = 10,
    Stella = 20,
    Unknown
}

Pourquoi ne lance-t-il pas une exception lors de la conversion d'un intqui est en dehors de ces valeurs en un type de Beer?

Par exemple, le code suivant ne lève pas d'exception, il renvoie '50' à la console:

int i = 50;
var b = (Beer) i;

Console.WriteLine(b.ToString());

Je trouve cela étrange ... quelqu'un peut-il clarifier?

jcvandan
la source
27
Notez que vous pouvez toujours vérifier si une valeur est valide avec Enum.IsDefined .
Tim Schmelter
cool je ne savais pas ça, je vais probablement l'utiliser dans le problème que j'essaie actuellement de résoudre
jcvandan
4
Mais lisez aussi ceci: goldfishforthought.blogspot.com/2008/03/…
Tim Schmelter
1
Voté pour avoir valu à Stella deux fois plus que Bud.
Dan Bechard

Réponses:

81

Tiré de la confusion avec l'analyse d'une énumération

C'était une décision de la part des personnes qui ont créé .NET. Un enum est soutenu par un autre type de valeur ( int, short,byte , etc.), et il peut effectivement avoir une valeur qui est valable pour ces types de valeur.

Personnellement, je ne suis pas fan de la façon dont cela fonctionne, j'ai donc créé une série de méthodes utilitaires:

/// <summary>
/// Utility methods for enum values. This static type will fail to initialize 
/// (throwing a <see cref="TypeInitializationException"/>) if
/// you try to provide a value that is not an enum.
/// </summary>
/// <typeparam name="T">An enum type. </typeparam>
public static class EnumUtil<T>
    where T : struct, IConvertible // Try to get as much of a static check as we can.
{
    // The .NET framework doesn't provide a compile-checked
    // way to ensure that a type is an enum, so we have to check when the type
    // is statically invoked.
    static EnumUtil()
    {
        // Throw Exception on static initialization if the given type isn't an enum.
        Require.That(typeof (T).IsEnum, () => typeof(T).FullName + " is not an enum type.");
    }

    /// <summary>
    /// In the .NET Framework, objects can be cast to enum values which are not
    /// defined for their type. This method provides a simple fail-fast check
    /// that the enum value is defined, and creates a cast at the same time.
    /// Cast the given value as the given enum type.
    /// Throw an exception if the value is not defined for the given enum type.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="enumValue"></param>
    /// <exception cref="InvalidCastException">
    /// If the given value is not a defined value of the enum type.
    /// </exception>
    /// <returns></returns>
    public static T DefinedCast(object enumValue)

    {
        if (!System.Enum.IsDefined(typeof(T), enumValue))
            throw new InvalidCastException(enumValue + " is not a defined value for enum type " +
                                           typeof (T).FullName);
        return (T) enumValue;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="enumValue"></param>
    /// <returns></returns>
    public static T Parse(string enumValue)
    {
        var parsedValue = (T)System.Enum.Parse(typeof (T), enumValue);
        //Require that the parsed value is defined
        Require.That(parsedValue.IsDefined(), 
            () => new ArgumentException(string.Format("{0} is not a defined value for enum type {1}", 
                enumValue, typeof(T).FullName)));
        return parsedValue;
    }

    public static bool IsDefined(T enumValue)
    {
        return System.Enum.IsDefined(typeof (T), enumValue);
    }

}


public static class EnumExtensions
{
    public static bool IsDefined<T>(this T enumValue)
        where T : struct, IConvertible
    {
        return EnumUtil<T>.IsDefined(enumValue);
    }
}

De cette façon, je peux dire:

if(!sEnum.IsDefined()) throw new Exception(...);

... ou:

EnumUtil<Stooge>.Parse(s); // throws an exception if s is not a defined value.

Éditer

Au-delà de l'explication donnée ci-dessus, vous devez vous rendre compte que la version .NET d'Enum suit un modèle plus inspiré du C que celui de Java. Cela permet d'avoir des énumérations "Bit Flag" qui peuvent utiliser des modèles binaires pour déterminer si un "drapeau" particulier est actif dans une valeur enum. Si vous deviez définir toutes les combinaisons possibles d'indicateurs (c'est MondayAndTuesday-à- dire MondayAndWednesdayAndThursday), ce serait extrêmement fastidieux. Ainsi, avoir la capacité d'utiliser des valeurs d'énumération non définies peut être très pratique. Cela nécessite juste un peu de travail supplémentaire lorsque vous voulez un comportement rapide sur les types enum qui ne tirent pas parti de ces types d'astuces.

StriplingGuerrier
la source
Bien ... je ne suis pas non plus fan de la façon dont cela fonctionne, cela me semble bizarre
jcvandan
3
@dormisher: "" C'est de la folie, mais il y a une méthode pour le faire. " Voir ma modification.
StriplingWarrior
2
@StriplingWarrior, pour l'intérêt. L'équivalent Java est EnumSet.of (lundi, mercredi, jeudi). À l'intérieur de EnumSet, il n'y a qu'un seul long. Donne donc une belle API sans trop de perte d'efficacité.
Iain
2
"Require.That ()" => stackoverflow.com/questions/4892548/…
Jean
@StriplingWarrior dans votre méthode d'analyse, vous essayez d'appeler IsDefined comme une méthode d'extension. Cependant, la méthode IsDefined ne peut pas être transformée en méthode d'extension en raison de la façon dont vous essayez de créer une classe EnumUtil avec des contraintes (pour effectuer une vérification statique de l'énumération, ce que j'aime beaucoup). cependant en fait cela me dit qu'on ne peut pas faire une méthode d'extension sur une classe générique. Des idées?
CJC
55

Les énumérations sont souvent utilisées comme indicateurs:

[Flags]
enum Permission
{
    None = 0x00,
    Read = 0x01,
    Write = 0x02,
}
...

Permission p = Permission.Read | Permission.Write;

La valeur de p est l'entier 3, qui n'est pas une valeur de l'énumération, mais est clairement une valeur valide.

Personnellement, j'aurais préféré voir une solution différente; J'aurais préféré avoir la capacité de faire des types entiers "bit array" et des types "un ensemble de valeurs distinctes" comme deux caractéristiques de langage différentes plutôt que de les fusionner tous les deux en "enum". Mais c'est ce que les concepteurs du langage et du cadre d'origine ont proposé; par conséquent, nous devons autoriser les valeurs non déclarées de l'énumération à être des valeurs légales.

Eric Lippert
la source
19
Mais 3 "est clairement une valeur valide" renforce en quelque sorte le point de vue du PO. Cet attribut [Flags] pourrait indiquer au compilateur de rechercher une énumération définie ou une valeur valide. Je dis juste.
LarsTech
15

La réponse courte: les concepteurs de langage ont décidé de concevoir le langage de cette façon.

La réponse longue: Section 6.2.2: Explicit enumeration conversionsde la spécification du langage C # dit:

Une conversion d'énumération explicite entre deux types est traitée en traitant tout type enum participant comme le type sous-jacent de ce type enum, puis en effectuant une conversion numérique implicite ou explicite entre les types résultants. Par exemple, étant donné un type enum E avec et le type sous-jacent int, une conversion de E en octet est traitée comme une conversion numérique explicite (§6.2.1) de int en octet, et une conversion d'octet en E est traitée comme une conversion numérique implicite (§6.1.2) d'octet en int.

Fondamentalement, l' énumération est traitée comme le type sous-jacent lorsqu'il s'agit d'effectuer une opération de conversion. Par défaut, le type sous-jacent d' une énumération est Int32, ce qui signifie que la conversion est traitée exactement comme une conversion en Int32. Cela signifie que toute intvaleur valide est autorisée.

Je soupçonne que cela a été fait principalement pour des raisons de performances. En créant enumun type intégral simple et en autorisant toute conversion de type intégral, le CLR n'a pas besoin de faire toutes les vérifications supplémentaires. Cela signifie que l'utilisation d'un enumn'a pas vraiment de perte de performances par rapport à l'utilisation d'un entier, ce qui contribue à encourager son utilisation.

Reed Copsey
la source
Trouvez-vous cela étrange? Je pensais que l'un des principaux points d'une énumération était de regrouper en toute sécurité une plage d'entiers que nous pouvons également attribuer des significations individuelles. Pour moi, autoriser un type enum à contenir n'importe quelle valeur entière va à l'encontre de son objectif.
jcvandan
@dormisher: Je viens de modifier et d'ajouter un peu à la fin. Il y a un coût à fournir la «sécurité» que vous recherchez. Cela étant dit, avec une utilisation normale , ce n'est vraiment pas du tout un problème.
Reed Copsey
9

De la documentation :

Une variable de type Days peut se voir affecter n'importe quelle valeur dans la plage du type sous-jacent; les valeurs ne sont pas limitées aux constantes nommées.

Martin
la source