2012-05-22 15 views
5

Sto scrivendo del codice con una semplice istruzione switch basata su valori Enum. Mi è venuto in mente che a un certo punto in futuro uno sviluppatore potrebbe aggiungere un nuovo valore, quindi ho incluso un metodo predefinito per catturarlo in fase di esecuzione e generare un'eccezione. Tuttavia mi sono reso conto che dovevo farlo ogni volta che inserivo una logica come questa, e che vedevo solo questi problemi in fase di esecuzione piuttosto che compilare il tempo.
Mi chiedo se è possibile aggiungere del codice per far sì che il compilatore comunichi allo sviluppatore che è necessario aggiornare determinati metodi nel caso in cui aggiornino i valori dell'enumerazione - oltre alla semplice aggiunta di commenti all'enum stesso?C# Garantire valori Validi Enum - Metodo a prova di futuro

ad es. (l'esempio sotto è puramente teorico: ho scelto gli stati dal ciclo di vita dello sviluppo per assicurarmi che sia qualcosa di familiare per la maggior parte).

public enum DevelopmentStatusEnum 
{ 
    Development 
    //, QA //this may be added at some point in the future (or any other status could be) 
    , SIT 
    , UAT 
    , Production 
} 

    public class Example 
    { 
     public void ExampleMethod(DevelopmentStatusEnum status) 
     { 
      switch (status) 
      { 
       case DevelopmentStatusEnum.Development: DoSomething(); break; 
       case DevelopmentStatusEnum.SIT: DoSomething(); break; 
       case DevelopmentStatusEnum.UAT: DoSomething(); break; 
       case DevelopmentStatusEnum.Production: DoSomething(); break; 
       default: throw new StupidProgrammerException(); //I'd like the compiler to ensure that this line never runs, even if a programmer edits the values available to the enum, alerting the program to add a new case statement for the new enum value 
      } 
     } 
     public void DoSomething() { } 
    } 
    public class StupidProgrammerException: InvalidOperationException { } 

Questo è un po 'accademico, ma posso vederlo utile per rendere robusta la mia app. Qualcuno l'ha già provato prima/ha qualche buona idea su come questo potrebbe essere raggiunto?

Grazie in anticipo,

JB

+2

Lo faccio nello stesso modo in cui lo fai tu. –

+2

Ogni volta che ti ritrovi a scrivere un'istruzione switch, dovresti prendere in considerazione il refactoring del codice nell'uso del polymorphism. [Vedi questo estratto] (http://sourcemaking.com/refactoring/replace-conditional-with-polymorphism) del libro di Martin Fowler [Refactoring] (http://martinfowler.com/books/refactoring.html). Ciò renderà il tuo codice aderente al principio Open-Closed (OCP) e aiuterà il prossimo sviluppatore ad evitare di mancare di cambiare un'istruzione switch da qualche parte. :-) –

risposta

11

In tal caso, proverei a non utilizzare un enum, ma piuttosto una classe con campi pubblici statici di sola lettura che sono istanze della classe. Guarda cosa fa il framework .Net con i colori, per esempio. C'è una classe Color e puoi usare oggetti come Color.Black, Color.Blue, ecc. Non sono costanti ma offrono quasi tutti gli stessi benefici. Inoltre hanno altri vantaggi che le costanti non hanno. Vedi la specifica della lingua C# versione 3 che parla anche di questo un po '.

Ma l'idea è che non si dispone di una dichiarazione di un caso. Si aggiungono abbastanza altre proprietà a ciascun membro "enum" che il metodo (DoSomething o qualsiasi altra cosa) può trattarlo correttamente. Quando un altro sviluppatore vuole aggiungere un altro membro, ha bisogno di fornire gli attributi necessari. Il mio esempio: avevo bisogno di un "enum" per le diverse azioni che un utente può eseguire nel sistema. Queste azioni dovevano essere controllate per le autorizzazioni, registrate, ecc. Avevo anche bisogno di azioni genitore e figlio (rinominare qualcosa è "parte di" modificarlo, ecc.), Azioni astratte usate per raggruppare azioni per scopi di filtraggio e azioni speciali " Tutto "e" Nessuno "(nessuno non definito). Ognuno aveva bisogno di un ID e di un testo nel database. Volevo che funzionasse ancora se qualcuno avesse inventato un nuovo tipo di azione. Quello che ho fatto è qualcosa del genere (un sacco di codice omesso solo per darti l'idea):

public class Action 
    { 
    protected Action(bool Abstract, Action Parent, int ID, string Name, bool Undefined) 
    { /* snip */ } 
    protected Action(bool Abstract, Action Parent, int ID, string Name) 
     : this(Abstract, Parent, ID, Name, false) 
    { } 
    //---------------------------------------------------------------------------------------- 
    public static readonly Action All = new Action(true, null, 0, "All"); 
    public static readonly Action None = new Action(false, All, 6, "(Undefined)", true); 
    public static readonly Action Modifying = new Action(true, All, 1, "Modifying"); 
    public static readonly Action Creating = new Action(false, Modifying, 2, "Creating"); 
    public static readonly Action Deleting = new Action(false, Modifying, 3, "Deleting"); 
    public static readonly Action Editing = new Action(false, Modifying, 4, "Editing"); 
    public static readonly Action Exporting = new Action(false, All, 5, "Exporting"); 
    public static readonly Action Renaming = new Action(false, Editing, 7, "Renaming"); 
    /* snip */ 
    //---------------------------------------------------------------------------------------- 
    /* template for new entries: 
    public static readonly Action = new Action(false, All, , ""); 
    */ 
    } 

Ci sono altre azioni. E ci sono un certo numero di metodi in altre classi che operano sulle azioni. Continuano a funzionare finché ogni azione fornisce le informazioni necessarie. Lo sviluppatore che aggiunge un'azione è obbligato a fornire le informazioni. Se gli attributi correnti non sono sufficienti per qualche futura azione "speciale", altri attributi dovranno essere aggiunti in seguito. Nota che i costruttori sono protetti, quindi solo la classe stessa può creare azioni. Ho omesso molto codice nel costruttore principale che controlla la presenza di ID e nomi duplicati e molte altre cose. È ora possibile utilizzarlo in questo modo:

Log.LogAction(Action.Renaming); 

Il metodo LogAction non ha un'istruzione case in esso. Usa gli attributi dell'azione.

Qual è il tuo elenco?

saluti

+0

Mi piace - non sono sicuro che funzioni per tutti i casi d'uso, ma sicuramente utile. Purtroppo non riesco a ricordare quale sia stato il mio caso di utilizzo originale che mi ha spinto a pensare a questa domanda - solo una delle tante volte in cui avevo bisogno di enumerazioni. Ora che conosco questo approccio cercherò di usarlo quando presentato con uno scenario simile a enum, assumendo che abbia senso farlo. Grazie ancora. – JohnLBevan

+0

ps. Ho appena usato questo per la prima volta nel rispondere a qualcun altro sulla domanda di questo - questa soluzione è molto meglio del mio precedente approccio - grazie. http://developer42.wordpress.com/2012/10/12/path-finder-in-c/ – JohnLBevan

+1

Solo una nota di nitpicker: in .NET, hai uno schema con due classi diverse, una singolare (come Colore o Pennello) e un plurale, che è statico (come colori o pennelli). Per i colori: http://msdn.microsoft.com/fr-fr/library/vstudio/system.windows.media.colors.black.aspx – jv42

2

Potrei sbagliarmi, ma non credo, il compilatore offre tali avvertimenti. È possibile rilevare tali problemi con alcuni test unitari, che invocano un metodo come quello sopra con tutti i possibili valori enum (utilizzare Enum.GetValues() per questo). Ogni volta, uno sviluppatore aggiunge un membro enum e dimentica di modificare tutte le istruzioni switch, almeno un test unitario avrà esito negativo con una "StupidProgrammerException" (btw: vorrei lanciare ArgumentOutOfRangeException).

+0

"InvalidEnumArgumentException" sarebbe più adeguato. –

3

Ho trovato una 'soluzione' utilizzando Enums pianura. Qui è, crudo, forse potrebbe essere migliorato con un design migliore:

bool allCasesHandled; 

switch (myEnumValue) 
{ 
    case MyEnum.Value1: 
     allCasesHandled = true; 
     break; 

    //default: 
    // allCasesHandled = true; 
    // break; 
} 
System.Diagnostics.Debug.WriteLine(allCasesHandled); 

Se si tenta di compilare questo, si otterrà un errore 'uso di una variabile non assegnata'.

Questo è un po 'macchinoso da mantenere, ma per casi semplici, potrebbe essere utile, specialmente con un commento sulla linea che è destinata a fallire.

+0

Bel trucco - dal momento che non avere un valore predefinito lascia un percorso in cui l'assegnazione delle variabili potrebbe essere saltata . Ho dato a @Roelof il segno di spunta in quanto ritengo che la sua sia la migliore architettura in assoluto, ma questo è un buon compromesso per ottenere ciò che ho richiesto senza troppi sovraccarichi. – JohnLBevan

+0

@JohnLBevan Grazie, il tuo voto ha senso, tutto dipende dallo scopo del codice. – jv42

0

Penso che sia possibile scrivere regole per StyleCop ed eseguirli in un evento PostBuild e possono generare avvisi nella finestra Costruisci. Abbiamo iniziato a provare questo tempo per aggiungere un avviso per ignorare il valore restituito da un metodo, ma non sono mai riuscito a completarlo. L'ultima volta che l'ho esaminata, era necessario analizzare l'IL, che non è sempre divertente. Certo, immagino che dipenda dalla tua definizione di divertimento.