Devo accettare raccolte vuote nei miei metodi che ripetono su di esse?


40

Ho un metodo in cui tutta la logica viene eseguita all'interno di un ciclo foreach che scorre sul parametro del metodo:

public IEnumerable<TransformedNode> TransformNodes(IEnumerable<Node> nodes)
{
    foreach(var node in nodes)
    {
        // yadda yadda yadda
        yield return transformedNode;
    }
}

In questo caso, l'invio di una raccolta vuota comporta una raccolta vuota, ma mi chiedo se non sia saggio.

La mia logica qui è che se qualcuno chiama questo metodo, allora intendono passare i dati e passerebbero una raccolta vuota al mio metodo in circostanze errate.

Devo prendere questo comportamento e lanciare un'eccezione per esso o è buona norma restituire la raccolta vuota?


43
Sei sicuro che la tua ipotesi "se qualcuno chiama questo metodo, allora intendono passare i dati" è corretta? Forse stanno solo passando ciò che hanno a portata di mano per lavorare con il risultato.
SpaceTrucker,

7
A proposito: il tuo metodo "trasforma" di solito è chiamato "mappa", sebbene in .NET (e SQL, da cui .NET ha preso il nome), è comunemente chiamato "seleziona". "Trasforma" è ciò che lo chiama C ++, ma non è un nome comune che si possa riconoscere.
Jörg W Mittag,

25
Getterei se la collezione è nullma non se è vuota.
CodesInChaos,

21
@NickUdell - Passo sempre raccolte vuote nel mio codice. È più facile per il codice comportarsi allo stesso modo di scrivere una logica di ramificazione speciale per i casi in cui non ho potuto aggiungere nulla alla mia raccolta prima di procedere con essa.
theMayer,

7
Se si controlla e si genera un errore se la raccolta è vuota, si sta forzando anche il chiamante a controllare e non passare una raccolta vuota al metodo. Le convalide dovrebbero essere fatte su (e solo su) confini del sistema, se le raccolte vuote ti danno fastidio, dovresti averle già verificate molto prima che raggiungesse qualsiasi metodo di utilità. Ora hai due controlli ridondanti.
Lie Ryan,

Risposte:


175

I metodi di utilità non devono essere lanciati su raccolte vuote. I tuoi client API ti odierebbero per questo.

Una raccolta può essere vuota; una "collezione-che-non-deve-essere-vuota" è concettualmente una cosa molto più difficile con cui lavorare.

La trasformazione di una raccolta vuota ha un risultato ovvio: la raccolta vuota. (È anche possibile salvare un po 'di immondizia restituendo il parametro stesso.)

Ci sono molte circostanze in cui un modulo mantiene elenchi di cose che possono o non possono essere già riempite con qualcosa. Dover controllare il vuoto prima di ogni chiamata transformè fastidioso e ha il potenziale per trasformare un algoritmo semplice ed elegante in un brutto pasticcio.

I metodi di utilità dovrebbero sempre sforzarsi di essere liberali nei loro input e conservatori nei loro output.

Per tutti questi motivi, per l'amor di Dio, gestire correttamente la raccolta vuota. Niente è più esasperante di un modulo helper che pensa di sapere cosa vuoi meglio di te.


14
+1 - (più se potessi). Potrei anche spingermi fino a fondermi anche nullnella raccolta vuota. Le funzioni con limitazioni implicite sono un dolore.
Telastyn,

6
"No, non dovresti" ... non dovresti accettarli o non lanciare un'eccezione?
Ben Aaronson,

16
@Andy - quelli non sarebbero metodi di utilità però. Sarebbero metodi commerciali o comportamenti concreti simili.
Telastyn,

38
Non penso che riciclare la collezione vuota per un ritorno sia una buona idea. Stai solo risparmiando un po 'di memoria; e potrebbe comportare un comportamento imprevisto nel chiamante. Esempio leggermente inventato: Populate1(input); output1 = TransformNodes(input); Populate2(input); output2 = TransformNodes(input); se Populate1 lasciava vuota la raccolta e la restituiva per la prima chiamata TransformNodes, output1 e input sarebbero la stessa raccolta, e quando veniva chiamato Populaten2 se mettesse nodi nella raccolta si finirebbe con il secondo insieme di input in output2.
Dan Neely,

6
@Andy Anche così, se l'argomento non dovrebbe mai essere vuoto - se è un errore non avere dati da elaborare - allora sento che è responsabilità del chiamante controllare questo, quando sta raccogliendo quell'argomento. Non solo mantiene questo metodo più elegante, ma significa che è possibile generare un messaggio di errore migliore (contesto migliore) e sollevarlo in modo rapido. Non ha senso per una classe a valle verificare gli invarianti del chiamante ...
Andrzej Doyle,

26

Vedo due domande importanti che determinano la risposta a questo:

  1. La tua funzione può restituire qualcosa di significativo e logico quando viene passata una raccolta vuota (incluso null )?
  2. Qual è lo stile di programmazione generale in questa applicazione / libreria / squadra? (In particolare, come FP sei?)

1. Ritorno significativo

In sostanza, se puoi restituire qualcosa di significativo, non gettare un'eccezione. Lascia che il chiamante gestisca il risultato. Quindi se la tua funzione ...

  • Conta il numero di elementi nella raccolta, restituisce 0. È stato facile.
  • Cerca elementi che soddisfano determinati criteri, restituisce una raccolta vuota. Per favore, non gettare nulla. Il chiamante potrebbe avere una vasta collezione di raccolte, alcune delle quali sono vuote, altre no. Il chiamante desidera qualsiasi elemento corrispondente in qualsiasi raccolta. Le eccezioni rendono solo più difficile la vita del chiamante.
  • Sta cercando il più grande / il più piccolo / il più adatto ai criteri nell'elenco. Ops. A seconda della domanda di stile, potresti generare un'eccezione o restituire null . Odio il null (molto una persona FP) ma qui è probabilmente più significativo e ti consente di riservare eccezioni per errori imprevisti nel tuo codice. Se il chiamante non verifica la presenza di null, si verificherà comunque un'eccezione abbastanza chiara. Ma lo lasci a loro.
  • Chiede l'ennesimo elemento nella raccolta o il primo / ultimo n elementi. Questo è il caso migliore per un'eccezione e quello che è meno probabile che causi confusione e difficoltà per il chiamante. Un caso può essere ancora reso nullo se tu e il tuo team siete abituati a verificarlo, per tutti i motivi indicati nel punto precedente, ma questo è il caso più valido per lanciare un'eccezione DudeYou Know YouShouldCheckTheSizeFirst . Se il tuo stile è più funzionale, allora null o continua a leggere la mia risposta di stile .

In generale, il mio pregiudizio FP mi dice "tornare a qualcosa di significativo" e null in questo caso può avere un significato valido.

2. Stile

Il tuo codice generale (o il codice del progetto o il codice del team) favorisce uno stile funzionale? In caso contrario, saranno previste e trattate eccezioni. Se sì, considera la possibilità di restituire un Tipo di opzione . Con un tipo di opzione, si restituisce una risposta significativa o Nessuno / Niente . Nel mio terzo esempio dall'alto, Niente sarebbe una buona risposta in stile FP. Il fatto che la funzione restituisca chiaramente al chiamante un tipo di Opzione che una risposta significativa potrebbe non essere possibile e che il chiamante dovrebbe essere pronto a gestirlo. Sento che offre al chiamante più opzioni (se perdonerai il gioco di parole).

F # è dove tutti il fresco .Net bambini fanno questo genere di cose, ma C # non sostenere questo stile.

tl; dr

Mantieni eccezioni per errori imprevisti nel tuo percorso di codice, input non completamente prevedibili (e legali) da parte di qualcun altro.


"Migliore adattamento", ecc .: È possibile che sia disponibile un metodo che trova l'oggetto più adatto in una raccolta che soddisfa alcuni criteri. Tale metodo avrebbe lo stesso problema per le raccolte non vuote, poiché nulla potrebbe soddisfare il criterio, quindi non è necessario preoccuparsi di selezioni vuote.
gnasher729,

1
No, è per questo che ho detto "la migliore forma" e non "le corrispondenze"; la frase implica l'approssimazione più vicina, non una corrispondenza esatta. Le opzioni più grandi / più piccole avrebbero dovuto essere un indizio. Se è stato richiesto solo il "miglior adattamento", qualsiasi raccolta con 1 o più membri dovrebbe essere in grado di restituire un membro. Se c'è un solo membro, è la soluzione migliore.
itsbruce,

1
Restituire "null" nella maggior parte dei casi è peggio che lanciare un'eccezione. Tuttavia, restituire una raccolta vuota è significativo.
Ian,

1
Non se devi restituire un singolo articolo (min / max / testa / coda). Per quanto riguarda null, come ho detto, lo odio (e preferisco le lingue che non lo hanno), ma dove una lingua lo possiede, devi essere in grado di gestirlo e codificarlo. A seconda delle convenzioni di codifica locali, potrebbe essere appropriato.
itsbruce,

Nessuno dei tuoi esempi ha nulla a che fare con un input vuoto. Sono solo casi speciali di situazioni in cui la risposta potrebbe essere "nulla". Che a sua volta dovrebbe rispondere alla domanda del PO su come "gestire" la raccolta vuota.
Djechlin,

18

Come sempre, dipende.

Ha importanza che la raccolta è vuota?
La maggior parte del codice di gestione delle raccolte probabilmente direbbe "no"; una raccolta può contenere un numero qualsiasi di elementi, incluso zero.

Ora, se hai una sorta di raccolta in cui è "non valido" non avere elementi in esso, allora questo è un nuovo requisito e devi decidere cosa fare al riguardo.

Prendi in prestito alcune logiche di test dal mondo del database: prova per zero oggetti, un oggetto e due oggetti. Ciò soddisfa i casi più importanti (eliminando qualsiasi condizione di unione interna o cartesiana mal formata).


E se non è valido non avere elementi in una raccolta, idealmente l'argomento non sarebbe una java.util.Collection, ma una com.foo.util.NonEmptyCollectionclasse personalizzata , che può costantemente preservare questo invariante e impedirti di iniziare con uno stato non valido.
Andrzej Doyle,

3
Questa domanda è taggata [c #]
Nick Udell il

@NickUdell C # non supporta la programmazione orientata agli oggetti o il concetto di spazi dei nomi nidificati? TIL.
John Dvorak,

2
Lo fa. Il mio commento è stato un chiarimento in quanto Andrzej deve essere stato confuso (per quale altro motivo dovrebbero andare allo sforzo di specificare lo spazio dei nomi per una lingua a cui questa domanda non fa riferimento?).
Nick Udell,

-1 per enfatizzare il "dipende" più del "quasi certamente sì". Non è uno scherzo: comunicare la cosa sbagliata qui fa male.
Djechlin,

11

Per una buona progettazione, accetta quante più variazioni nei tuoi input pratiche o possibili. Le eccezioni devono essere generate solo quando (viene presentato un input inaccettabile O si verificano errori imprevisti durante l'elaborazione) E, di conseguenza, il programma non può continuare in modo prevedibile .

In questo caso, dovrebbe essere previsto che venga presentata una raccolta vuota e il codice deve gestirla (cosa che già fa). Sarebbe una violazione di tutto ciò che è buono se il tuo codice ha generato un'eccezione qui. Ciò sarebbe simile a moltiplicare 0 per 0 in matematica. È ridondante, ma assolutamente necessario affinché funzioni come funziona.

Ora, passiamo all'argomento null collection. In questo caso, una raccolta nulla è un errore di programmazione: il programmatore ha dimenticato di assegnare una variabile. Questo è un caso in cui è possibile generare un'eccezione, poiché non è possibile elaborarla in modo significativo in un output e tentare di farlo introdurrebbe un comportamento imprevisto. Ciò sarebbe simile alla divisione per zero in matematica - è completamente insignificante.


La tua affermazione audace è un consiglio estremamente negativo in generale. Penso che sia vero qui, ma è necessario spiegare cosa c'è di speciale in questa circostanza. (È un cattivo consiglio in generale perché promuove fallimenti silenziosi.)
djechlin

@AAA - chiaramente non stiamo scrivendo un libro su come progettare software qui, ma non credo affatto che sia un cattivo consiglio se capisci davvero quello che sto dicendo. Il punto è che se un input errato produce output ambigui o arbitrari, è necessario generare un'eccezione. Nei casi in cui l'output è totalmente prevedibile (come nel caso qui), lanciare un'eccezione sarebbe arbitrario. La chiave non è mai prendere decisioni arbitrarie nel tuo programma, poiché è da lì che nasce un comportamento imprevedibile.
theMayer

Ma è la tua prima frase e l'unica evidenziata ... nessuno capisce ancora cosa stai dicendo. (Non sto cercando di essere troppo aggressivo qui, solo una delle cose che siamo qui per fare è imparare a scrivere e comunicare e faccio cose che la perdita di precisione in un punto chiave è dannosa.)
Djechlin

10

La soluzione giusta è molto più difficile da vedere quando guardi la tua funzione da solo. Considera la tua funzione come una parte di un problema più grande . Una possibile soluzione a questo esempio è simile a questa (in Scala):

input.split("\\D")
.filterNot (_.isEmpty)
.map (_.toInt)
.filter (x => x >= 1000 && x <= 9999)

Innanzitutto, dividi la stringa per non cifre, filtra le stringhe vuote, converti le stringhe in numeri interi, quindi filtra per mantenere solo i numeri a quattro cifre. La tua funzione potrebbe essere map (_.toInt)nella pipeline.

Questo codice è abbastanza semplice perché ogni fase della pipeline gestisce solo una stringa vuota o una raccolta vuota. Se si inserisce una stringa vuota all'inizio, alla fine viene visualizzato un elenco vuoto. Non è necessario interrompere e verificare nullo un'eccezione dopo ogni chiamata.

Naturalmente, presumendo che un elenco di output vuoto non abbia più di un significato. Se è necessario distinguere tra un output vuoto causato da input vuoto e uno causato dalla trasformazione stessa, ciò cambia completamente le cose.


+1 per un esempio estremamente efficace (privo delle altre buone risposte).
Djechlin,

2

Questa domanda riguarda in realtà le eccezioni. Se lo guardi in questo modo e ignori la raccolta vuota come dettaglio di implementazione, la risposta è semplice:

1) Un metodo dovrebbe generare un'eccezione quando non può procedere: o impossibile eseguire l'attività designata o restituire il valore appropriato.

2) Un metodo dovrebbe rilevare un'eccezione quando è in grado di procedere nonostante il fallimento.

Pertanto, il metodo di supporto non dovrebbe essere "utile" e generare un'eccezione a meno che non sia in grado di eseguire il lavoro con una raccolta vuota. Consentire al chiamante di determinare se i risultati possono essere gestiti.

Che restituisca una raccolta vuota o nulla, è un po 'più difficile ma non molto: le raccolte nullable dovrebbero essere evitate se possibile. Lo scopo di una raccolta nullable sarebbe quello di indicare (come in SQL) che non si hanno le informazioni - una raccolta di figli, ad esempio, potrebbe essere nulla se non si sa se qualcuno ne ha, ma non si sapere che non lo fanno. Ma se questo è importante per qualche motivo, probabilmente vale la pena una variabile extra per rintracciarlo.


1

Il metodo è chiamato TransformNodes. Nel caso di una raccolta vuota come input, tornare a una raccolta vuota è naturale e intuitivo e ha un senso matematico perfetto.

Se il metodo è stato nominato Maxe progettato per restituire l'elemento massimo, sarebbe naturale buttare NoSuchElementExceptionsu una raccolta vuota, poiché il massimo di niente non ha alcun senso matematico.

Se il metodo è stato denominato JoinSqlColumnNamese progettato per restituire una stringa in cui gli elementi sono uniti da una virgola da utilizzare nelle query SQL, avrebbe senso lanciare IllegalArgumentExceptionuna raccolta vuota, poiché il chiamante alla fine otterrebbe comunque un errore SQL se lo usasse la stringa in una query SQL direttamente senza ulteriori controlli e invece di verificare la presenza di una stringa vuota restituita, avrebbe dovuto invece verificare la raccolta vuota.


Maxdel nulla è spesso l'infinito negativo.
Djechlin,

0

Facciamo un passo indietro e usiamo un esempio diverso che calcola la media aritmetica di una matrice di valori.

Se l'array di input è vuoto (o null), è possibile soddisfare ragionevolmente la richiesta del chiamante? No. Quali sono le tue opzioni? Bene, potresti:

  • presenta / ritorna / genera un errore. usando la convenzione del tuo codebase per quella classe di errore.
  • documentare che verrà restituito un valore come zero
  • documento che verrà restituito un valore non valido designato (ad es. NaN)
  • documentare che verrà restituito un valore magico (ad es. un minimo o massimo per il tipo o un valore eventualmente indicativo)
  • dichiarare che il risultato non è specificato
  • dichiarare che l'azione non è definita
  • eccetera.

Dico di dare loro l'errore se ti hanno dato un input non valido e la richiesta non può essere completata. Intendo un errore grave dal primo giorno in modo che capiscano i requisiti del tuo programma. Dopotutto, la tua funzione non è in grado di rispondere. Se l'operazione potrebbe non riuscire (ad esempio, copiare un file), l'API dovrebbe fornire loro un errore che possono gestire.

In questo modo è possibile definire il modo in cui la libreria gestisce richieste non valide e richieste che potrebbero non riuscire.

È molto importante che il codice sia coerente nel modo in cui gestisce queste classi di errori.

La categoria successiva è decidere come la tua libreria gestisce le richieste senza senso. Tornando ad un esempio simile al vostro - usiamo una funzione che determina se esiste un file in un percorso: bool FileExistsAtPath(String). Se il client passa una stringa vuota, come gestite questo scenario? Che ne dici di un array vuoto o nullo passato a void SaveDocuments(Array<Document>)? Decidi per la tua libreria / base di codice e sii coerente. Mi capita di considerare questi casi come errori e vietare ai clienti di fare richieste senza senso segnalandoli come errori (tramite un'asserzione). Alcune persone resisteranno fortemente a quell'idea / azione. Trovo questo rilevamento degli errori molto utile. È molto utile per individuare i problemi nei programmi, con una buona località rispetto al programma offensivo. I programmi sono molto più chiari e corretti (considera l'evoluzione della tua base di codice) e non bruciano cicli all'interno di funzioni che non fanno nulla. Il codice è più piccolo / più pulito in questo modo e i controlli vengono generalmente trasferiti nei punti in cui il problema può essere introdotto.


6
Per il tuo ultimo esempio, non è compito di quella funzione determinare ciò che non ha senso. Pertanto, una stringa vuota dovrebbe restituire false. In effetti, questo è l'approccio esatto adottato da File.Exists .
theMayer,

@ rmayer06 è il suo lavoro. la funzione di riferimento consente stringhe nulle, vuote, verifica la presenza di caratteri non validi nella stringa, esegue il troncamento della stringa, potrebbe essere necessario effettuare chiamate al filesystem, potrebbe essere necessario interrogare l'ambiente, ecc. alto costo e sicuramente sfocano le linee di correttezza (IMO). ho visto molti if (!FileExists(path)) { ...create it!!!... }bug - molti dei quali sarebbero stati catturati prima del commit, se le linee di correttezza non fossero sfocate.
appena il

2
Sarei d'accordo con te, se il nome della funzione fosse File.ThrowExceptionIfPathIsInvalid, ma chi nella loro mente corretta chiamerebbe tale funzione?
the May

Una media di 0 elementi restituirà già NaN o genererà un'eccezione di divisione per zero, proprio a causa della definizione della funzione. Un file con un nome che non può eventualmente esistere, o non esisterà o genererà già un errore. Non c'è motivo convincente per gestire questi casi in modo speciale.
cHao,

Si potrebbe anche sostenere che l'intero concetto di verificare l'esistenza di un file prima di leggerlo o scriverlo, sia comunque difettoso. (C'è una condizione di razza intrinseca.) Meglio andare avanti e provare ad aprire il file, per la lettura o con le impostazioni di sola creazione se si scrive. Avrà già esito negativo se il nome file non è valido o non esiste (o esiste, nel caso di scrittura).
cHao,

-3

Come regola generale, una funzione standard dovrebbe essere in grado di accettare l'elenco più ampio di input e fornire feedback su di esso, ci sono molti esempi in cui i programmatori usano le funzioni in modi che il progettista non aveva pianificato, con questo in mente credo che la funzione dovrebbe essere in grado di accettare non solo raccolte vuote ma una vasta gamma di tipi di input e restituire con garbo feedback, sia esso anche un oggetto di errore da qualsiasi operazione eseguita sull'input ...


È assolutamente sbagliato per il progettista supporre che una collezione verrà sempre passata e passerà direttamente all'iterazione su detta collezione, un controllo deve essere fatto per assicurarsi che l'argomento ricevuto sia conforme al tipo di dati previsto ...
Clement Mark-Aaba

3
"ampia gamma di tipi di input" sembra irrilevante qui. La domanda è taggata c # , un linguaggio fortemente tipizzato. Cioè, il compilatore garantisce che l'input è una raccolta
moscerino del

Gentilmente "regola empirica" ​​di Google, la mia risposta è stata una diffida avvertenza che come programmatore si fa spazio a eventi imprevisti o errati, uno di questi può essere erroneamente superato argomenti di funzione ...
Clemente Mark-Aaba

1
Questo è sbagliato o tautologico. writePaycheckToEmployeenon deve accettare un numero negativo come input ... ma se "feedback" significa "fa qualcosa che potrebbe succedere dopo", allora sì, ogni funzione farà qualcosa che farà dopo.
Djechlin,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.