Eccezione vs set di risultati vuoti quando gli input sono tecnicamente validi, ma insoddisfacenti

108

Sto sviluppando una libreria destinata alla pubblicazione pubblica. Contiene vari metodi per operare su insiemi di oggetti - generare, ispezionare, partizionare e proiettare gli insiemi in nuove forme. Nel caso sia rilevante, è una libreria di classi C # contenente estensioni stile LINQ su IEnumerable , da rilasciare come pacchetto NuGet.

Alcuni dei metodi in questa libreria possono ricevere parametri di input insoddisfacenti. Ad esempio, nei metodi combinatori, esiste un metodo per generare tutti gli insiemi di elementi n che possono essere costruiti da un set sorgente di elementi m . Ad esempio, dato il set:

1, 2, 3, 4, 5

e chiedere combinazioni di 2 produrrebbe:

1, 2
1, 3
1, 4
etc...
5, 3
5, 4

Ora, è ovviamente possibile chiedere qualcosa che non può essere fatto, come dargli un set di 3 elementi e poi chiedere combinazioni di 4 elementi mentre si imposta l'opzione che dice che può usare ogni oggetto una sola volta.

In questo scenario, ogni parametro è individualmente valido:

  • La raccolta di origine non è nullo e contiene elementi
  • La dimensione richiesta di combinazioni è un intero diverso da zero positivo
  • La modalità richiesta (usa ogni oggetto una sola volta) è una scelta valida

Tuttavia, lo stato dei parametri quando presi insieme causa problemi.

In questo scenario, ti aspetteresti che il metodo generi un'eccezione (ad esempio InvalidOperationException ) o restituisca una raccolta vuota? O sembra valido per me:

  • Non puoi produrre combinazioni di elementi n da un insieme di elementi m dove n > m se ti è consentito utilizzare ogni oggetto solo una volta, quindi questa operazione può essere ritenuta impossibile, quindi InvalidOperationException .
  • L'insieme di combinazioni di dimensioni n che possono essere prodotte da elementi m quando n > m è un set vuoto; nessuna combinazione può essere prodotta.

L'argomento per un set vuoto

La mia prima preoccupazione è che un'eccezione impedisce il concatenamento di metodi in stile LINQ idiomatico quando si ha a che fare con set di dati che possono avere dimensioni sconosciute. In altre parole, potresti voler fare qualcosa di simile:

var result = someInputSet
    .CombinationsOf(4, CombinationsGenerationMode.Distinct)
    .Select(combo => /* do some operation to a combination */)
    .ToList();

Se il tuo set di input è di dimensioni variabili, il comportamento di questo codice è imprevedibile. Se .CombinationsOf() genera un'eccezione quando someInputSet ha meno di 4 elementi, allora questo codice a volte fallirà in fase di esecuzione senza alcuni controlli preliminari. Nell'esempio precedente questo controllo è banale, ma se lo chiami a metà di una catena più lunga di LINQ, questo potrebbe diventare noioso. Se restituisce un set vuoto, result sarà vuoto, il che potresti essere perfettamente soddisfatto.

L'argomento per un'eccezione

La mia seconda preoccupazione è che la restituzione di un set vuoto potrebbe nascondere problemi: se si chiama questo metodo a metà di una catena di LINQ e viene restituito un set vuoto in modo silenzioso, è possibile che si verifichino problemi qualche passo dopo o si trovi con un set di risultati vuoto, e potrebbe non essere ovvio come ciò è accaduto dato che hai sicuramente qualcosa nel set di input.

Che cosa ti aspetti e qual è il tuo argomento?

    
posta anaximander 30.11.2016 - 13:19
fonte

13 risposte

145

Restituisci un set vuoto

Mi aspetto un set vuoto perché:

Ci sono 0 combinazioni di 4 numeri dal set di 3 quando posso usare ogni numero solo una volta

    
risposta data 30.11.2016 - 14:54
fonte
80

In caso di dubbio, chiedi a qualcun altro.

La tua funzione di esempio ha uno molto simile in Python: itertools.combinations . Vediamo come funziona:

>>> import itertools
>>> input = [1, 2, 3, 4, 5]
>>> list(itertools.combinations(input, 2))
[(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)]
>>> list(itertools.combinations(input, 5))
[(1, 2, 3, 4, 5)]
>>> list(itertools.combinations(input, 6))
[]

E mi sembra perfettamente a posto. Mi aspettavo un risultato che potessi ripetere e ne ho ricevuto uno.

Ma, ovviamente, se dovessi chiedere qualcosa di stupido:

>>> list(itertools.combinations(input, -1))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: r must be non-negative

Quindi direi che se tutti i parametri sono validi, ma il risultato è un set vuoto restituisce un set vuoto, non sei l'unico a farlo.

Come detto da @ Bakuriu nei commenti, questo è anche lo stesso per una query SQL come SELECT <columns> FROM <table> WHERE <conditions> . Finché <columns> , <table> , <conditions> sono ben formati e si riferiscono a nomi esistenti, è possibile creare un insieme di condizioni che si escludono a vicenda. La query risultante non produrrebbe solo righe anziché generare InvalidConditionsError .

    
risposta data 30.11.2016 - 14:55
fonte
72

In parole povere:

  • Se c'è un errore, dovresti sollevare un'eccezione. Ciò potrebbe comportare il fare le cose in fasi invece che in una singola chiamata concatenata per sapere esattamente dove si è verificato l'errore.
  • Se non ci sono errori ma il set risultante è vuoto, non generare un'eccezione, restituire il set vuoto. Un set vuoto è un insieme valido.
risposta data 30.11.2016 - 13:32
fonte
53

Sono d'accordo con la risposta di Ewan ma voglio aggiungere un ragionamento specifico.

Hai a che fare con operazioni matematiche, quindi potrebbe essere un buon consiglio attenersi alle stesse definizioni matematiche. Da un punto di vista matematico il numero di r -set di un n -set (cioè nCr ) è ben definito per tutti i r > n > = 0. È zero. Pertanto restituire un set vuoto sarebbe il caso previsto da un punto di vista matematico.

    
risposta data 30.11.2016 - 15:36
fonte
24

Trovo che un buon metodo per determinare se utilizzare un'eccezione, è immaginare le persone coinvolte nella transazione.

Esegui il recupero dei contenuti di un file come esempio:

  1. Per favore, scarica il contenuto del file, "does not exist.txt"

    a. "Ecco i contenuti: una raccolta di caratteri vuota"

    b. "Ehm, c'è un problema, quel file non esiste. Non so cosa fare!"

  2. Per favore, scarica il contenuto del file, "esiste ma è empty.txt"

    a. "Ecco i contenuti: una raccolta di caratteri vuota"

    b. "Ehm, c'è un problema, non c'è niente in questo file. Non so cosa fare!"

Senza dubbio alcuni non saranno d'accordo, ma per la maggior parte delle persone, "Erm, c'è un problema" ha senso quando il file non esiste e restituisce "una raccolta di caratteri vuota" quando il file è vuoto.

Quindi applica lo stesso approccio al tuo esempio:

  1. Per favore dammi tutte le combinazioni di 4 elementi per {1, 2, 3}

    a. Non ce ne sono, ecco un set vuoto.

    b. C'è un problema, non so cosa fare.

Anche in questo caso, "C'è un problema" avrebbe senso se ad esempio null fosse offerto come set di elementi, ma "ecco un set vuoto" sembra una risposta ragionevole alla richiesta di cui sopra.

Se restituire un valore vuoto maschera un problema (ad es. un file mancante, null ), allora dovrebbe essere usata generalmente un'eccezione (a meno che la lingua scelta supporti% tipi di% co_de, allora a volte hanno più senso). In caso contrario, restituire un valore vuoto probabilmente semplificherà il costo e si conformerà meglio al principio del minimo stupore.

    
risposta data 30.11.2016 - 14:48
fonte
10

Poiché è per una biblioteca di carattere generale, il mio istinto sarebbe Lascia scegliere all'utente finale

Proprio come abbiamo Parse() e TryParse() a nostra disposizione, possiamo avere l'opzione che usiamo a seconda dell'output che ci serve dalla funzione. Trascorrere meno tempo a scrivere e mantenere un wrapper di funzioni per generare un'eccezione piuttosto che discutere sulla scelta di una singola versione della funzione.

    
risposta data 30.11.2016 - 15:36
fonte
4

Devi convalidare gli argomenti forniti quando viene chiamata la tua funzione. E in effetti, vuoi sapere come gestire argomenti non validi. Il fatto che più argomenti dipendano l'uno dall'altro, non compensa il fatto che tu convalidi gli argomenti.

Pertanto voterei per l'ArgumentException fornendo le informazioni necessarie all'utente per capire cosa è andato storto.

Ad esempio, controlla il public static TSource ElementAt<TSource>(this IEnumerable<TSource>, Int32) funzione in Linq. Che genera un'eccezione ArgumentOutOfRangeException se l'indice è minore di 0 o maggiore o uguale al numero di elementi in origine. Pertanto, l'indice è convalidato rispetto all'enumerabile fornito dal chiamante.

    
risposta data 30.11.2016 - 15:04
fonte
2

Riesco a vedere gli argomenti per entrambi i casi d'uso - un'eccezione è grande se il codice downstream si aspetta degli insiemi che contengono dati. D'altra parte, semplicemente un set vuoto è ottimo se questo è previsto.

Penso che dipenda dalle aspettative del chiamante se si tratta di un errore o di un risultato accettabile, quindi trasferirò la scelta al chiamante. Forse introdurre un'opzione?

.CombinationsOf(4, CombinationsGenerationMode.Distinct, Options.AllowEmptySets)

    
risposta data 30.11.2016 - 14:44
fonte
2

Ci sono due approcci per decidere se non c'è una risposta ovvia:

  • Scrivi il codice assumendo prima una opzione, poi l'altra. Considera quale sarebbe il migliore nella pratica.

  • Aggiungi un parametro booleano "strict" per indicare se vuoi che i parametri siano rigorosamente verificati o meno. Ad esempio, SimpleDateFormat di Java ha un metodo setLenient per tentare di analizzare gli input che non corrispondono completamente al formato. Ovviamente, dovresti decidere quale sia l'impostazione predefinita.

risposta data 30.11.2016 - 15:35
fonte
2

Sulla base della tua analisi, restituire il set vuoto sembra chiaramente giusto - l'hai persino identificato come qualcosa che alcuni utenti potrebbero volere e non sono caduto nella trappola di vietarne l'uso perché non puoi immaginare che gli utenti volessero mai per usarlo in questo modo.

Se ritieni davvero che alcuni utenti vogliano forzare ritorni non vuoti, allora offri loro un modo per chiedere per quel comportamento piuttosto che forzarlo su tutti. Ad esempio, potresti:

  • Renderlo un'opzione di configurazione su qualunque oggetto stia eseguendo l'azione per l'utente.
  • Crea una bandiera che l'utente può facoltativamente passare alla funzione.
  • Fornisci un controllo di AssertNonempty che possono inserire nelle loro catene.
  • Crea due funzioni, una che afferma non vuoto e una che non lo fa.
risposta data 01.12.2016 - 03:55
fonte
2

Dovresti eseguire una delle seguenti azioni (sebbene continui a utilizzare in modo coerente problemi di base come un numero negativo di combinazioni):

  1. Fornire due implementazioni, una che restituisce un insieme vuoto quando gli input insieme sono privi di senso e uno che lancia. Prova a chiamarli CombinationsOf e CombinationsOfWithInputCheck . O qualunque cosa ti piaccia. Puoi invertirlo in modo che l'input-checking sia il nome più corto e l'elenco uno sia CombinationsOfAllowInconsistentParameters .

  2. Per i metodi Linq, restituisci il IEnumerable vuoto sulla premessa esatta che hai delineato. Quindi, aggiungi questi metodi Linq alla tua libreria:

    public static class EnumerableExtensions {
       public static IEnumerable<T> ThrowIfEmpty<T>(this IEnumerable<T> input) {
          return input.IfEmpty<T>(() => {
             throw new InvalidOperationException("An enumerable was unexpectedly empty");
          });
       }
    
       public static IEnumerable<T> IfEmpty<T>(
          this IEnumerable<T> input,
          Action callbackIfEmpty
       ) {
          var enumerator = input.GetEnumerator();
          if (!enumerator.MoveNext()) {
             // Safe because if this throws, we'll never run the return statement below
             callbackIfEmpty();
          }
          return EnumeratePrimedEnumerator(enumerator);
       }
    
       private static IEnumerable<T> EnumeratePrimedEnumerator<T>(
          IEnumerator<T> primedEnumerator
       ) {
          yield return primedEnumerator.Current;
          while (primedEnumerator.MoveNext()) {
             yield return primedEnumerator.Current;
          }
       }
    }
    

    Infine, usalo in questo modo:

    var result = someInputSet
       .CombinationsOf(4, CombinationsGenerationMode.Distinct)
       .ThrowIfEmpty()
       .Select(combo => /* do some operation to a combination */)
       .ToList();
    

    o così:

    var result = someInputSet
       .CombinationsOf(4, CombinationsGenerationMode.Distinct)
       .IfEmpty(() => _log.Warning(
          [email protected]"Unexpectedly received no results when creating combinations for {
             nameof(someInputSet)}"
       ))
       .Select(combo => /* do some operation to a combination */)
       .ToList();
    

    Si noti che il metodo privato è diverso da quelli pubblici è necessario per il comportamento di lancio o di azione che si verifica quando viene creata la catena di linq invece di un po 'di tempo dopo quando viene enumerata. vuoi da buttare subito.

    Nota, tuttavia, che ovviamente deve enumerare almeno il primo elemento per determinare se ci sono articoli. Questo è un potenziale svantaggio che penso sia per lo più mitigato dal fatto che qualsiasi spettatore futuro può facilmente ragionare che un metodo ThrowIfEmpty ha per enumerare almeno un elemento, quindi non sii sorpreso dal farlo. Ma non lo sai mai. Puoi renderlo più esplicito ThrowIfEmptyByEnumeratingAndReEmittingFirstItem . Ma sembra un gigantesco overkill.

Penso che il # 2 sia abbastanza, beh, fantastico! Ora il potere è nel codice chiamante, e il prossimo lettore del codice capirà esattamente cosa sta facendo, e non avrà a che fare con eccezioni impreviste.

    
risposta data 01.12.2016 - 21:47
fonte
1

Dipende molto da ciò che i tuoi utenti si aspettano di ottenere. Per esempio (un po 'non correlato) se il tuo codice esegue la divisione, puoi lanciare un'eccezione o restituire Inf o NaN quando dividi per zero. Né è giusto o sbagliato, tuttavia:

  • se restituisci Inf in una libreria Python, le persone ti assalgono per nascondere gli errori
  • se si genera un errore in una libreria Matlab, le persone ti assalgono per non aver elaborato i dati con valori mancanti

Nel tuo caso, sceglierei la soluzione che sarebbe meno sorprendente per gli utenti finali. Dato che stai sviluppando una libreria che si occupa di set, un set vuoto sembra qualcosa che i tuoi utenti si aspetterebbero di affrontare, quindi restituirlo sembra una cosa sensata da fare. Ma potrei sbagliarmi: tu hai una migliore comprensione del contesto rispetto a chiunque altro qui, quindi se ti aspetti che i tuoi utenti facciano affidamento sul set sempre non vuoto, dovresti immediatamente generare un'eccezione.

Soluzioni che permettono all'utente di scegliere (come aggiungere un parametro "strict") non sono definitive, poiché sostituiscono la domanda originale con una nuova equivalente: "Quale valore di strict dovrebbe essere il default? "

    
risposta data 01.12.2016 - 11:48
fonte
0

È una concezione comune (in matematica) che quando selezioni elementi su un insieme non trovi nessun elemento e quindi ottieni un set vuoto . Ovviamente devi essere coerente con la matematica se vai così:

Regole comuni:

  • Set.Foreach (predicato); // restituisce sempre true per gli insiemi vuoti
  • Set.Exists (predicato); // restituisce sempre false per gli insiemi vuoti

La tua domanda è molto sottile:

  • Potrebbe essere che l'input della tua funzione abbia rispettare un contratto : In tal caso, qualsiasi input non valido dovrebbe generare un'eccezione, ovvero, la funzione non funziona con parametri regolari.

  • Potrebbe essere che l'input della tua funzione debba comportarsi in modo esattamente come un set , e quindi dovrebbe essere in grado di restituire un set vuoto.

Ora, se fossi in te, userei il metodo "Set", ma con un grande "MA".

Supponi di avere una collezione che "per ipotesi" dovrebbe avere solo studentesse:

class FemaleClass{

    FemaleStudent GetAnyFemale(){
        var femaleSet= mySet.Select( x=> x.IsFemale());
        if(femaleSet.IsEmpty())
            throw new Exception("No female students");
        else
            return femaleSet.Any();
    }
}

Ora la tua collezione non è più un "set puro", perché hai un contratto su di esso, e quindi dovresti applicare il tuo contratto con un'eccezione.

Quando usi le funzioni "set" in modo puro, non devi generare eccezioni in caso di set vuoto, ma se hai collezioni che non sono più "set puri" allora dovresti lanciare eccezioni dove appropriato .

Dovresti sempre fare ciò che ti sembra più naturale e coerente: per me un set dovrebbe rispettare le regole, mentre le cose che non sono set dovrebbero avere le loro regole correttamente pensate.

Nel tuo caso sembra una buona idea fare:

List SomeMethod( Set someInputSet){
    var result = someInputSet
        .CombinationsOf(4, CombinationsGenerationMode.Distinct)
        .Select(combo => /* do some operation to a combination */)
        .ToList();

    // the only information here is that set is empty => there are no combinations

    // BEWARE! if 0 here it may be invalid input, but also a empty set
    if(result.Count == 0)  //Add: "&&someInputSet.NotEmpty()"

    // we go a step further, our API require combinations, so
    // this method cannot satisfy the API request, then we throw.
         throw new Exception("you requsted impossible combinations");

    return result;
}

Ma non è davvero una buona idea, ora abbiamo uno stato non valido che può verificarsi in fase di esecuzione in momenti casuali, tuttavia questo è implicito nel problema quindi non possiamo rimuoverlo, certo che possiamo spostare l'eccezione all'interno di alcuni metodo di utilità (è esattamente lo stesso codice, spostato in luoghi diversi), ma è sbagliato e in pratica la cosa migliore che puoi fare è stick alle regole regolari del set .

Infatti l'aggiunta di nuove complessità solo per mostrare che puoi scrivere i metodi di query di linq sembra non vale per il tuo problema , sono abbastanza sicuro che se OP ci può dire di più sul suo dominio, probabilmente potremmo trovare il punto in cui l'eccezione è realmente necessaria (se possibile, è possibile che il problema non richieda alcuna eccezione).

    
risposta data 01.12.2016 - 13:20
fonte

Leggi altre domande sui tag