2015-07-22 16 views
6

Provengo dal Wild Wild West di PHP e Javascript dove è possibile restituire qualsiasi cosa da una funzione. Anche se odio questa mancanza di responsabilità, devo anche affrontare una nuova sfida nel tentativo di mantenere il mio codice "perfetto".Come evitare problemi di intervallo in funzioni generiche

ho fatto questa funzione generica per scegliere un elemento casuale da un elenco

public static T PickRandom<T>(this IList<T> list) { 
    Random random = new Random(); 
    int rnd = random.Next(list.Count); 
    return list[rnd]; 
} 

ma voglio proteggermi da utilizzarlo in un elenco con i valori 0. Ovviamente non posso restituire nulla da questa funzione diversa da T, come false o -1. Posso ovviamente fare questo

if(myList.Count > 0) 
    foo = Utilites.PickRandom(myList); 

Tuttavia ci sono così tante cose pazzesche in C# che io non conosco, e per questo app sto creando io molto, molto spesso devo scegliere un elemento casuale da un lista che potrebbe essere costantemente decrementata nel suo conteggio. C'è un modo migliore?

+4

cosa dovrebbe succedere se un po 'si fa chiamare questo metodo su una lista vuota? – Rob

+0

Suppongo che non dovrebbe restituire nulla, ma che non è possibile? – user3822370

+1

Se l'elenco contiene tipi di riferimento (oggetti, ad esempio), è possibile farlo. Ma questo non funziona per cose come 'int' a meno che tu non dica esplicitamente che è null-able. Mi sembra che sia ** invalid ** chiamare questo metodo su una lista vuota, quindi un'eccezione dovrebbe andare bene. Restituire null potrebbe essere inaspettato per il chiamante. Vorrei lanciare un'eccezione se conta == 0 nel metodo PickRandom. Inoltre, dovresti usare questo: 'random.Next (list.Count - 1)', oppure puoi ottenere un'eccezione cercando di accedere all'elemento 'last + 1'. – Rob

risposta

8

Le opzioni che avete sono

return default(T) 

che sarà un comportamento ambiguo, in quanto questo potrebbe essere un elemento valido della lista.

Oppure è possibile restituire qualcosa come -1 come hai detto, ma questo è abbastanza abbinato al tuo codice.

Oppure è possibile restituire null, ma ciò può essere fatto solo se T è un tipo nullable, ovviamente.

In tutti i casi precedenti, se il chiamante non è a conoscenza di questa situazione, l'applicazione potrebbe continuare con un valore non valido, che porta a conseguenze sconosciute.

Quindi probabilmente l'opzione migliore è quella di generare un'eccezione:

throw new InvalidOperationException(); 

Con questo approccio, si riesce veloce e assicurarsi che nulla imprevisto si verifica oltre le intenzioni del chiamante.

Un motivo in più per eseguire il backup di questa opzione. Prendiamo ad esempio i metodi di estensione di Linq. Se chiami First(), Single() o Last() in una lista vuota, riceverai un InvalidOperationException con il messaggio "La sequenza non contiene elementi". Dare alla tua classe un comportamento simile alle classi del framework 'è sempre una buona cosa.


Aggiungo una nota a margine grazie al commento di Alexei Levenkov nella domanda. Quella generazione casuale non è l'approccio migliore. Dai un'occhiata a this question.


Seconda nota a margine. Stai dichiarando la tua funzione come metodo di estensione per IList<T> (lo fai usando il this prima del primo parametro) ma poi lo chiami come un metodo di supporto statico.Un metodo di estensione è uno zucchero sintattico che, invece di fare questo:

foo = Utilites.PickRandom(myList); 

ti permette di fare questo:

foo = myList.PickRandom(); 

Maggiori informazioni sui metodi di estensione può essere trovato here.

+4

Direi che questa è la risposta corretta.Tuttavia, vorrei che sottolinei i problemi nel restituire un valore distinto. Vale a dire, il fatto che alcuni chiamanti si dimenticheranno quasi sicuramente di controllare il valore. Pertanto, consentire all'indicizzatore di lanciare 'IndexOutOfRangeException' è probabilmente la mossa migliore (che hai già indicato). –

+1

Grazie per questa risposta. Ora ho solo difficoltà a vedere come il chiamante gestisce questo. Il PickRandom lancia un'eccezione, la gestirò in qualche modo, ma cosa fa il chiamante? Come si sa a cauzione? Ha bisogno del suo tentativo/cattura? – user3822370

+1

Sì, il chiamante deve essere consapevole di questa situazione, voi (come il metodo di estensione) non sono incaricati di decidere cosa fare in questo caso. Quindi dovrebbe controllare il conteggio delle liste prima di chiamare o fare un tentativo di cattura. Data questa situazione, il primo approccio è migliore, in quanto una lista vuota è qualcosa che potresti aspettarti e non qualcosa di imprevedibile (è lì che dovresti usare try-catches). – Andrew

0

Un'altra alternativa è la seguente coppia di sovraccarichi invece dell'originale. Con questi, dovrebbe essere chiaro al chiamante che devono fornire un valore casuale predefinito nel caso in cui uno non possa essere "selezionato" dall'elenco.

public static T PickRandomOrReturnDefault<T>(this IList<T> list, T defaultRandomValue) 
{ 
    if (list == null || list.Count == 0) return defaultRandomValue; 

    Random random = new Random(); 
    int rnd = random.Next(list.Count); 
    return list[rnd]; 
} 

public static T PickRandomOrReturnDefault<T>(this IList<T> list, Func<T> createRandomValue) 
{ 
    if (list == null || list.Count == 0) return createRandomValue(); 

    Random random = new Random(); 
    int rnd = random.Next(list.Count); 
    return list[rnd]; 
} 

Nota: Probabilmente si dovrebbe considerare la possibilità di un campo casuale membro statico della classe piuttosto che ri-istanziare più e più volte. Vedere la risposta per questo post Correct method of a "static" Random.Next in C#?

0

Un'altra opzione è utilizzare la monade Maybe<T>. È molto simile a Nullable<T>, ma funziona con i tipi di riferimento.

public class Maybe<T> 
{ 
    public readonly static Maybe<T> Nothing = new Maybe<T>(); 

    public T Value { get; private set; } 
    public bool HasValue { get; private set; } 

    public Maybe() 
    { 
     HasValue = false; 
    } 

    public Maybe(T value) 
    { 
     Value = value; 
     HasValue = true; 
    } 

    public static implicit operator Maybe<T>(T v) 
    { 
     return v.ToMaybe(); 
    } 
} 

Il vostro codice potrebbe quindi simile a questa:

private static Random random = new Random(); 
public static Maybe<T> PickRandom<T>(this IList<T> list) 
{ 
    var result = Maybe<T>.Nothing; 
    if (list.Any()) 
    { 
     result = list[random.Next(list.Count)].ToMaybe(); 
    } 
    return result; 
} 

si dovrebbe utilizzare come:

var item = list.PickRandom(); 

if (item.HasValue) { ... } 

Personalmente, il nome della mia forse metodi con forse alla fine.

Sarebbe più come questo:

var itemMaybe = list.PickRandomMaybe(); 

if (itemMaybe.HasValue) { ... } 
Problemi correlati