2009-11-18 16 views
35

C'è qualcosa che non riesco a capire in C#. Puoi lanciare un valore fuori scala int in un enum e il compilatore non sussulta. Immaginate questo enum:Casting to enums in C#

enum Colour 
{ 
    Red = 1, 
    Green = 2, 
    Blue = 3 
} 

Ora, se si scrive:

Colour eco; 
eco = (Colour)17; 

Il compilatore pensa che va bene. E anche il runtime. Uh?

Perché il team C# ha deciso di rendere questo possibile? Questa decisione non coglie il punto di utilizzo di enumerazioni, credo, in scenari come questo:

void DoSomethingWithColour(Colour eco) 
{ 
    //do something to eco. 
} 

In un linguaggio forte tipizzato come C#, vorrei pensare che eco sarà sempre contenere un valore legale Colour. Ma questo non è il caso. Un programmatore può chiamare il mio metodo con un valore di 17 assegnato a eco (come nel frammento di codice precedente), quindi il codice nel mio metodo non deve presupporre che eco detenga un valore legale Colour. Ho bisogno di testarlo esplicitamente e gestire i valori eccezionali come mi pare. Perchè è questo?

A mio modesto parere, sarebbe molto più bello se il compilatore ha emesso un messaggio di errore (o anche un avvertimento) quando lancia un out-of gamma int in un enum, se il valore int è noto al momento della compilazione. In caso contrario, il runtime dovrebbe generare un'eccezione nell'istruzione di assegnazione.

Cosa ne pensi? C'è qualche ragione per cui è così?

(. Nota Questa è una domanda I posted ages ago on my blog ma non ottenne risposta informativo.)

risposta

31

Indovinare di 'perché' è sempre pericoloso, ma considerare questo:

enum Direction { North =1, East = 2, South = 4, West = 8 } 
Direction ne = Direction.North | Direction.East; 

int value = (int) ne; // value == 3 
string text = ne.ToString(); // text == "3" 

Quando l'attributo [Flags] è messo in davanti alla enum, che ultima riga cambia a

string text = ne.ToString(); // text == "North, East" 
+0

Il tuo punto di essere? :-) Se vuoi rendere SouthEast una direzione valida, penso che dovresti includerlo nella dichiarazione enum come SouthEast = 10, ad esempio. Essere d'accordo? – CesarGon

+2

Sono chiamate enumerazioni di bandiere. Design molto vaild. –

+20

No, non sono d'accordo. I campi di bit sono un uso perfettamente valido di un enum, tanto che l'attributo [Flags] non è nemmeno richiesto di usarli come tali. –

3

Quando si definisce un enum si sono essenzialmente dando nomi ai valori (s syntatic ugar se vuoi). Quando lanci 17 su Colore, stai memorizzando un valore per un Colore senza nome. Come probabilmente saprai, alla fine è solo un campo int.

Questo è simile alla dichiarazione di un numero intero che accetta solo valori da 1 a 100; l'unica lingua che abbia mai visto che supportava questo livello di controllo era FREDDO.

+0

Non concordo. Forse nei tempi antichi del C++ era lo zucchero sintattico. Ma in .NET, le enumerazioni sono di prima classe. Questo è il punto di avere tipi di enumerazione! – CesarGon

+0

Potete immaginare l'overhead per il CLR per verificare l'enumerazione valida? Penso che alla fine abbiano preso la decisione di lasciarlo deselezionato perché non valeva la pena di controllarlo. –

+0

Che ne dici del fatto che non puoi più usare le enumerazioni dei flag? –

4

Non è necessario gestire le eccezioni. Il presupposto per il metodo è che i chiamanti debbano usare l'enum, non lanciare alcuna int willy nilly al suddetto enum. Sarebbe una follia. Non è il punto delle enumerazioni non utilizzare gli ints?

Qualsiasi dev che volesse gettare 17 sull'enum dei colori avrebbe bisogno di 17 calci sul didietro per quanto mi riguarda.

+0

Concordato sui calci. :-D Ma la programmazione difensiva è una buona pratica. E quando stai facendo interfacce pubbliche per la tua libreria di classi, * sai * dovresti controllare i parametri di input. – CesarGon

+0

Quindi si imposta un valore predefinito su uno switch e si genera un'eccezione o si interrompe il debugger. Ci sono molti casi vaild per questi valori interi dalle enumerazioni. –

+0

@CesarGon: Quindi controllali tu stesso, nell'esempio da dare tutto quello che devi controllare è che il valore sia compreso tra 1 e 3. CmdrTallen ha un commento che mostra come controllare in generale. – tloach

4

Questo è uno dei molti motivi per cui non si dovrebbero mai assegnare valori interi alle proprie enumerazioni. Se hanno valori importanti che devono essere usati in altre parti del codice, trasforma l'enum in un oggetto.

+1

Ci sono anche molti motivi per includere i valori. Potresti interagire con un sistema esterno e usare semplicemente l'enumerazione per rendere il tuo codice più pulito. –

+2

Non penso che ci siano "molte" ragioni. Comprendo l'argomento dell'interfaccia con i sistemi esterni, certo. Ma non c'è modo di sostenere che un enum con valori assegnati produrrà un codice più pulito, più simile al codice smellier. Puoi seguire un semplice modello di oggetti di valore e creare oggetti di facile comprensione ... e, sai, seguire l'intera cosa di OOP? –

+3

Quando si trasferiscono dati da e verso il DB è necessario forzare enum da int a enum e viceversa. – Guy

-1

Si genera un errore utilizzando Enum.Parse();

Enum parsedColour = (Colour)Enum.Parse(typeof(Colour), "17"); 

Potrebbe essere utile utilizzarlo per ottenere un errore di runtime generato, se lo si desidera.

+2

Per quanto posso vedere, questo in realtà non funziona. Imposta parsedColour su 17, anziché lanciare un'eccezione. Viene generata un'eccezione se si passa una stringa casuale (es. "Asdf"). O mi sta sfuggendo qualcosa? –

+0

Dopo aver provato, posso anche convenire che questo non funziona. Questo non genera un errore di runtime. Puoi verificarlo da solo effettuando un rapido test unitario. – Gilles

1

Versione corta:

Non farlo.

Cercando di modificare enum in int con solo consentendo valori validi (forse un fallback predefinito) richiede metodi di supporto. A quel punto non hai un enum - hai davvero una lezione.

Doubly così se gli inte sono improtant - come ha detto Bryan Rowe.

15

Non sono sicuro del perché, ma di recente ho trovato questa "funzione" incredibilmente utile. Ho scritto qualcosa del genere l'altro giorno

// a simple enum 
public enum TransmissionStatus 
{ 
    Success = 0, 
    Failure = 1, 
    Error = 2, 
} 
// a consumer of enum 
public class MyClass 
{ 
    public void ProcessTransmissionStatus (TransmissionStatus status) 
    { 
     ... 
     // an exhaustive switch statement, but only if 
     // enum remains the same 
     switch (status) 
     { 
      case TransmissionStatus.Success: ... break; 
      case TransmissionStatus.Failure: ... break; 
      case TransmissionStatus.Error: ... break; 
      // should never be called, unless enum is 
      // extended - which is entirely possible! 
      // remember, code defensively! future proof! 
      default: 
       throw new NotSupportedException(); 
       break; 
     } 
     ... 
    } 
} 

la domanda è, come posso testare la clausola dell'ultimo caso? È del tutto ragionevole supporre che qualcuno possa estendere TransmissionStatus e non aggiornare i suoi consumatori, come il povero piccolo MyClass sopra. Tuttavia, mi piacerebbe comunque verificarne il comportamento in questo scenario. Un modo è quello di utilizzare il casting, come

[Test] 
[ExpectedException (typeof (NotSupportedException))] 
public void Test_ProcessTransmissionStatus_ExtendedEnum() 
{ 
    MyClass myClass = new MyClass(); 
    myClass.ProcessTransmissionStatus ((TransmissionStatus)(10)); 
} 
+1

Sei serio? Trovo che un trucco veloce e sporco, con tutto il dovuto rispetto. :-) – CesarGon

+4

Quindi ... presumendo che le enumerazioni fossero completamente sicure con la validazione del valore, come andresti a testare il condizionale precedente? Preferiresti aggiungere un valore "Unsupported" in ogni enum che hai mai scritto con l'esplicito scopo di testare le clausole predefinite? Se mi chiedi, questo puzza come una puzza molto più grande;) –

+1

In effetti, un valore non supportato in un enum è esplicitamente chiaro. Penso che sarebbe un modo molto migliore per fare i test, sì. :-) – CesarGon

5

Certamente vedo il punto di Cesar, e ricordo che inizialmente mi confondevo anche io. Secondo me le enumerazioni, nella loro attuale implementazione, sono davvero un po 'troppo basse e perdenti. Mi sembra che ci sarebbero due soluzioni al problema.

1) Consentire l'archiviazione di valori arbitrari in un enum solo se la sua definizione ha il valore FlagsAttribute. In questo modo, possiamo continuare a usarli per una maschera di bit quando appropriato (e dichiarato esplicitamente), ma quando usati semplicemente come segnaposto per le costanti avremmo ottenuto il controllo del valore in fase di esecuzione.

2) Introdurre un tipo primitivo separato chiamato say, maschera di bit, che consentirebbe qualsiasi valore ulong. Di nuovo, limitiamo le enumerazioni standard ai soli valori dichiarati. Ciò avrebbe l'ulteriore vantaggio di consentire al compilatore di assegnare i valori di bit per te. Quindi questo:

[Flags] 
enum MyBitmask 
{ 
    FirstValue = 1, SecondValue = 2, ThirdValue = 4, FourthValue = 8 
} 

sarebbe equivalente a questo:

bitmask MyBitmask 
{ 
    FirstValue, SecondValue, ThirdValue, FourthValue 
} 

Dopo tutto, i valori per qualsiasi maschera di bit sono completamente prevedibili, giusto? Come programmatore, sono più che felice di aver tolto questo dettaglio.

Ancora, troppo tardi ora, credo che siamo bloccati con l'implementazione attuale per sempre. :/

+0

Grazie, Mikey. Questo è ciò che intendo quando dico che * real * enum e bitfield (enunciati "flag") sono in realtà due tipi di cose molto diversi. Il linguaggio C++ e le lingue correlate li hanno considerati la stessa cosa, e temo che C# abbia ereditato quel difetto. Ma la semantica è chiaramente diversa. Forse Eric Lippert o Jon Skeet potrebbero passare e far luce su questo problema. – CesarGon

0

ho pensato di condividere il codice ho finito per usare per convalidare enumerazioni, come finora non sembrano avere nulla qui che funziona ...

public static class EnumHelper<T> 
{ 
    public static bool IsValidValue(int value) 
    { 
     try 
     { 
      Parse(value.ToString()); 
     } 
     catch 
     { 
      return false; 
     } 

     return true; 
    } 

    public static T Parse(string value) 
    { 
     var values = GetValues(); 
     int valueAsInt; 
     var isInteger = Int32.TryParse(value, out valueAsInt); 
     if(!values.Select(v => v.ToString()).Contains(value) 
      && (!isInteger || !values.Select(v => Convert.ToInt32(v)).Contains(valueAsInt))) 
     { 
      throw new ArgumentException("Value '" + value + "' is not a valid value for " + typeof(T)); 
     } 

     return (T)Enum.Parse(typeof(T), value); 
    } 

    public static bool TryParse(string value, out T p) 
    { 
     try 
     { 
      p = Parse(value); 
      return true; 
     } 
     catch (Exception) 
     { 
      p = default(T); 
      return false; 
     } 

    } 

    public static IEnumerable<T> GetValues() 
    { 
     return Enum.GetValues(typeof (T)).Cast<T>(); 
    } 
} 
4

Questo è inaspettato .. . ciò che vogliamo veramente è quello di controllare il casting ... per esempio:

Colour eco; 
if(Enum.TryParse("17", out eco)) //Parse successfully?? 
{ 
    var isValid = Enum.GetValues(typeof (Colour)).Cast<Colour>().Contains(eco); 
    if(isValid) 
    { 
    //It is really a valid Enum Colour. Here is safe! 
    } 
} 
3
if (!Enum.IsDefined(typeof(Colour), 17)) 
{ 
    // Do something 
} 
0

vecchia questione, ma questo mi ha confuso di recente, e Google mi ha portato qui.Ho trovato un articolo con ulteriori ricerche che finalmente mi ha fatto clic e ho pensato di tornare e condividere.

L'essenza è che Enum è una struttura, il che significa che è un tipo di valore (source). Ma in sostanza si comporta come un tipo derivato del tipo sottostante (int, byte, long, ecc.). Quindi, se riesci a pensare ad un tipo Enum proprio come il suo tipo sottostante con alcune funzionalità aggiuntive/zucchero sintattico (come ha detto Otávio), allora sarai consapevole di questo "problema" e sarai in grado di proteggerti.

Parlando di questo, qui è il cuore di un metodo di estensione che ho scritto per convertire facilmente/analizzare le cose a un Enum:

if (value != null) 
{ 
    TEnum result; 
    if (Enum.TryParse(value.ToString(), true, out result)) 
    { 
     // since an out-of-range int can be cast to TEnum, double-check that result is valid 
     if (Enum.IsDefined(typeof(TEnum), result.ToString())) 
     { 
      return result; 
     } 
    } 
} 

// deal with null and defaults... 

La variabile value v'è di tipo di oggetto, in quanto questa estensione ha "overload" che accetta int, int ?, string, Enum ed Enum ?. Sono tutti in scatola e inviati al metodo privato che esegue l'analisi. Usando sia TryParse e IsDefinednell'ordine, posso analizzare tutti i tipi appena menzionati, incluse le stringhe miste, ed è piuttosto robusto.

utilizzo è in questo modo (si assume NUnit):

[Test] 
public void MultipleInputTypeSample() 
{ 
    int source; 
    SampleEnum result; 

    // valid int value 
    source = 0; 
    result = source.ParseToEnum<SampleEnum>(); 
    Assert.That(result, Is.EqualTo(SampleEnum.Value1)); 

    // out of range int value 
    source = 15; 
    Assert.Throws<ArgumentException>(() => source.ParseToEnum<SampleEnum>()); 

    // out of range int with default provided 
    source = 30; 
    result = source.ParseToEnum<SampleEnum>(SampleEnum.Value2); 
    Assert.That(result, Is.EqualTo(SampleEnum.Value2)); 
} 

private enum SampleEnum 
{ 
    Value1, 
    Value2 
} 

Speranza che aiuta qualcuno.

responsabilità: non usiamo le bandiere/enumerazioni bitmask ... questo non è stato testato con quello scenario di utilizzo e, probabilmente, non avrebbe funzionato.