È una specie di domanda generale (ma sto usando C #), qual è il modo migliore (best practice), restituisci una collezione nulla o vuota per un metodo che ha una raccolta come tipo di ritorno?
È una specie di domanda generale (ma sto usando C #), qual è il modo migliore (best practice), restituisci una collezione nulla o vuota per un metodo che ha una raccolta come tipo di ritorno?
Risposte:
Raccolta vuota. Sempre.
Questo fa schifo:
if(myInstance.CollectionProperty != null)
{
foreach(var item in myInstance.CollectionProperty)
/* arrgh */
}
È considerata una procedura consigliata non restituire MAI null
quando si restituisce una raccolta o un elenco. Restituire SEMPRE un enumerable / collection vuoto. Previene le suddette assurdità e impedisce che la tua auto venga colpita da colleghi e utenti delle tue classi.
Quando parli di proprietà, imposta sempre una volta la proprietà e dimenticala
public List<Foo> Foos {public get; private set;}
public Bar() { Foos = new List<Foo>(); }
In .NET 4.6.1, puoi condensare abbastanza:
public List<Foo> Foos { get; } = new List<Foo>();
Quando si parla di metodi che restituiscono enumerabili, è possibile restituire facilmente un enumerabile vuoto anziché null
...
public IEnumerable<Foo> GetMyFoos()
{
return InnerGetFoos() ?? Enumerable.Empty<Foo>();
}
L'uso Enumerable.Empty<T>()
può essere visto come più efficiente della restituzione, ad esempio, una nuova raccolta o matrice vuota.
IEnumerable
o ICollection
meno così importante. Ad ogni modo, se selezioni qualcosa di tipo ICollection
anche loro ritornano null
... Vorrei che restituissero una raccolta vuota, ma mi sono imbattuto in loro ritornando null
, quindi ho pensato di menzionarlo qui. Direi che il valore predefinito di una raccolta di enumerabili è vuoto, non nullo. Non sapevo che fosse un argomento così delicato.
Dalle Framework Design Guidelines 2nd Edition (pag. 256):
NON restituire valori null dalle proprietà della raccolta o dai metodi che restituiscono raccolte. Restituisce invece una raccolta vuota o una matrice vuota.
Ecco un altro articolo interessante sui vantaggi di non restituire null (stavo cercando di trovare qualcosa sul blog di Brad Abram, e lui si è collegato all'articolo).
Modifica- come Eric Lippert ha ora commentato la domanda originale, vorrei anche collegare il suo eccellente articolo .
Dipende dal tuo contratto e dal tuo caso concreto . Generalmente è meglio restituire raccolte vuote , ma a volte ( raramente ):
null
potrebbe significare qualcosa di più specifico;null
.Alcuni esempi concreti:
null
significherebbe che manca l'elemento, mentre una raccolta vuota renderebbe ridondante (e possibilmente errato)<collection />
C'è un altro punto che non è stato ancora menzionato. Considera il seguente codice:
public static IEnumerable<string> GetFavoriteEmoSongs()
{
yield break;
}
Il linguaggio C # restituirà un enumeratore vuoto quando chiama questo metodo. Pertanto, per essere coerente con il design del linguaggio (e, quindi, le aspettative del programmatore), è necessario restituire una raccolta vuota.
Vuoto è molto più adatto ai consumatori.
Esiste un metodo chiaro per creare un enumerabile vuoto:
Enumerable.Empty<Element>()
Mi sembra che dovresti restituire il valore semanticamente corretto nel contesto, qualunque esso sia. Una regola che dice "restituisci sempre una raccolta vuota" mi sembra un po 'semplicistica.
Supponiamo, diciamo, in un sistema per un ospedale, che abbiamo una funzione che dovrebbe restituire un elenco di tutti i precedenti ricoveri negli ultimi 5 anni. Se il cliente non è stato in ospedale, ha senso restituire un elenco vuoto. Ma cosa succede se il cliente lascia vuota quella parte del modulo di ammissione? Abbiamo bisogno di un valore diverso per distinguere "lista vuota" da "nessuna risposta" o "non so". Potremmo generare un'eccezione, ma non è necessariamente una condizione di errore e non ci porta necessariamente fuori dal normale flusso del programma.
Sono stato spesso frustrato da sistemi che non sono in grado di distinguere tra zero e nessuna risposta. Ho avuto diverse volte in cui un sistema mi ha chiesto di inserire un numero, inserire zero e viene visualizzato un messaggio di errore che mi dice che devo inserire un valore in questo campo. Ho appena fatto: ho inserito zero! Ma non accetterà zero perché non può distinguerlo da nessuna risposta.
Rispondi a Saunders:
Sì, presumo che ci sia una differenza tra "La persona non ha risposto alla domanda" e "La risposta è stata zero". Questo era il punto dell'ultimo paragrafo della mia risposta. Molti programmi non sono in grado di distinguere "non lo so" da vuoto o zero, il che mi sembra un difetto potenzialmente grave. Ad esempio, stavo facendo shopping per una casa circa un anno fa. Sono andato su un sito web immobiliare e c'erano molte case elencate con un prezzo richiesto di $ 0. Mi è sembrato abbastanza buono: stanno regalando queste case gratuitamente! Ma sono sicuro che la triste realtà era che non avevano inserito il prezzo. In quel caso potresti dire: "Beh, OBVIOUSLY zero significa che non hanno inserito il prezzo - nessuno regalerà una casa gratuitamente". Ma il sito ha anche elencato i prezzi medi di domanda e vendita di case in varie città. Non posso fare a meno di chiedermi se la media non includesse gli zeri, dando così una media erroneamente bassa per alcuni luoghi. cioè qual è la media di $ 100.000; $ 120.000 e "non lo so"? Tecnicamente la risposta è "non lo so". Quello che probabilmente vogliamo davvero vedere è $ 110.000. Ma quello che probabilmente otterremo è $ 73.333, che sarebbe completamente sbagliato. Inoltre, se avessimo questo problema su un sito in cui gli utenti possono ordinare online? (Improbabile per gli immobili, ma sono sicuro che tu l'abbia visto fatto per molti altri prodotti.) Vorremmo davvero che il "prezzo non ancora specificato" venga interpretato come "gratuito"? dando così una media erroneamente bassa per alcuni luoghi. cioè qual è la media di $ 100.000; $ 120.000 e "non lo so"? Tecnicamente la risposta è "non lo so". Quello che probabilmente vogliamo davvero vedere è $ 110.000. Ma quello che probabilmente otterremo è $ 73.333, che sarebbe completamente sbagliato. Inoltre, se avessimo questo problema su un sito in cui gli utenti possono ordinare online? (Improbabile per gli immobili, ma sono sicuro che tu l'abbia visto fatto per molti altri prodotti.) Vorremmo davvero che il "prezzo non ancora specificato" venga interpretato come "gratuito"? dando così una media erroneamente bassa per alcuni luoghi. cioè qual è la media di $ 100.000; $ 120.000 e "non lo so"? Tecnicamente la risposta è "non lo so". Quello che probabilmente vogliamo davvero vedere è $ 110.000. Ma quello che probabilmente otterremo è $ 73.333, che sarebbe completamente sbagliato. Inoltre, se avessimo questo problema su un sito in cui gli utenti possono ordinare online? (Improbabile per gli immobili, ma sono sicuro che tu l'abbia visto fatto per molti altri prodotti.) Vorremmo davvero che il "prezzo non ancora specificato" venga interpretato come "gratuito"? che sarebbe completamente sbagliato. Inoltre, se avessimo questo problema su un sito in cui gli utenti possono ordinare online? (Improbabile per gli immobili, ma sono sicuro che tu l'abbia visto fatto per molti altri prodotti.) Vorremmo davvero che il "prezzo non ancora specificato" venga interpretato come "gratuito"? che sarebbe completamente sbagliato. Inoltre, se avessimo questo problema su un sito in cui gli utenti possono ordinare online? (Improbabile per gli immobili, ma sono sicuro che tu l'abbia visto fatto per molti altri prodotti.) Vorremmo davvero che il "prezzo non ancora specificato" venga interpretato come "gratuito"?
RE con due funzioni separate, un "ce n'è?" e un "se sì, che cos'è?" Sì, sicuramente potresti farlo, ma perché dovresti farlo? Ora il programma chiamante deve effettuare due chiamate anziché una. Cosa succede se un programmatore non riesce a chiamare il "qualsiasi?" e va dritto al "che cos'è?" ? Il programma restituirà uno zero errato? Generare un'eccezione? Restituire un valore indefinito? Crea più codice, più lavoro e più potenziali errori.
L'unico vantaggio che vedo è che ti consente di rispettare una regola arbitraria. C'è qualche vantaggio in questa regola che vale la pena di obbedire? In caso contrario, perché preoccuparsi?
Rispondi a Jammycakes:
Considera come sarebbe il codice reale. So che la domanda diceva C # ma mi scusi se scrivo Java. Il mio C # non è molto nitido e il principio è lo stesso.
Con un ritorno null:
HospList list=patient.getHospitalizationList(patientId);
if (list==null)
{
// ... handle missing list ...
}
else
{
for (HospEntry entry : list)
// ... do whatever ...
}
Con una funzione separata:
if (patient.hasHospitalizationList(patientId))
{
// ... handle missing list ...
}
else
{
HospList=patient.getHospitalizationList(patientId))
for (HospEntry entry : list)
// ... do whatever ...
}
In realtà è una riga o due in meno di codice con il ritorno null, quindi non è più un onere per il chiamante, è meno.
Non vedo come si crea un problema SECCO. Non è che dobbiamo eseguire la chiamata due volte. Se avessimo sempre voluto fare la stessa cosa quando l'elenco non esiste, forse potremmo spingere la gestione verso il basso fino alla funzione get-list piuttosto che fare in modo che il chiamante lo faccia, e quindi inserire il codice nel chiamante sarebbe una violazione DRY. Ma quasi sicuramente non vogliamo fare sempre la stessa cosa. Nelle funzioni in cui è necessario disporre dell'elenco da elaborare, un elenco mancante è un errore che potrebbe interrompere l'elaborazione. Ma su una schermata di modifica, sicuramente non vogliamo interrompere l'elaborazione se non hanno ancora inserito dati: vogliamo consentire loro di inserire i dati. Pertanto, la gestione di "nessuna lista" deve essere eseguita a livello di chiamante in un modo o nell'altro. E se lo facciamo con un ritorno nullo o una funzione separata non fa differenza per il principio più grande.
Certo, se il chiamante non controlla null, il programma potrebbe fallire con un'eccezione puntatore null. Ma se esiste una funzione "get any" separata e il chiamante non chiama quella funzione ma chiama ciecamente la funzione "get list", allora cosa succede? Se genera un'eccezione o altrimenti fallisce, beh, è praticamente lo stesso di ciò che accadrebbe se restituisse null e non lo verificasse. Se restituisce un elenco vuoto, è semplicemente sbagliato. Non riesci a distinguere tra "Ho un elenco con zero elementi" e "Non ho un elenco". È come restituire zero per il prezzo quando l'utente non ha inserito alcun prezzo: è semplicemente sbagliato.
Non vedo come sia utile allegare un attributo aggiuntivo alla raccolta. Il chiamante deve ancora controllarlo. In che modo è meglio che verificare la presenza di null? Ancora una volta, la cosa peggiore in assoluto che potrebbe accadere è che il programmatore dimentichi di controllarlo e di dare risultati errati.
Una funzione che restituisce null non è una sorpresa se il programmatore ha familiarità con il concetto di significato null "non avere un valore", di cui penso che qualsiasi programmatore competente avrebbe dovuto sentir parlare, sia che pensi che sia una buona idea o meno. Penso che avere una funzione separata sia più un problema di "sorpresa". Se un programmatore non ha familiarità con l'API, quando esegue un test senza dati scoprirà rapidamente che a volte viene restituito un valore nullo. Ma come avrebbe scoperto l'esistenza di un'altra funzione se non gli fosse venuto in mente che potrebbe esserci una tale funzione e controlla la documentazione e la documentazione è completa e comprensibile? Preferirei di gran lunga avere una funzione che mi dà sempre una risposta significativa, piuttosto che due funzioni che devo conoscere e ricordare di chiamare entrambe.
Se una raccolta vuota ha senso semanticamente, è quello che preferisco restituire. Restituire una raccolta vuota per GetMessagesInMyInbox()
comunica "in realtà non ci sono messaggi nella posta in arrivo", mentre la restituzione null
potrebbe essere utile per comunicare che sono disponibili dati insufficienti per dire come dovrebbe apparire l'elenco che potrebbe essere restituito.
null
valore sicuramente non sembra ragionevole, stavo pensando in termini più generali su quello. Le eccezioni sono ottime anche per comunicare il fatto che qualcosa è andato storto, ma se i "dati insufficienti" citati sono perfettamente previsti, allora lanciare un'eccezione avrebbe un design scadente. Sto piuttosto pensando a uno scenario in cui è perfettamente possibile e nessun errore per il metodo a volte non essere in grado di calcolare una risposta.
La restituzione di null potrebbe essere più efficiente, poiché non viene creato alcun nuovo oggetto. Tuttavia, spesso richiederebbe anche un null
controllo (o una gestione delle eccezioni).
Semanticamente null
e un elenco vuoto non significano la stessa cosa. Le differenze sono sottili e una scelta potrebbe essere migliore dell'altra in casi specifici.
Indipendentemente dalla tua scelta, documentalo per evitare confusione.
Si potrebbe sostenere che il ragionamento dietro Null Object Pattern è simile a quello a favore della restituzione della raccolta vuota.
Dipende dalla situazione. Se si tratta di un caso speciale, restituire null. Se la funzione restituisce semplicemente una raccolta vuota, ovviamente la restituzione è ok. Tuttavia, restituire una raccolta vuota come caso speciale a causa di parametri non validi o altri motivi NON è una buona idea, perché sta mascherando una condizione di caso speciale.
In realtà, in questo caso di solito preferisco lanciare un'eccezione per assicurarmi che NON sia VERAMENTE ignorato :)
Dire che rende il codice più robusto (restituendo una raccolta vuota) in quanto non devono gestire la condizione nulla è male, in quanto è semplicemente mascherare un problema che dovrebbe essere gestito dal codice chiamante.
Direi che null
non è la stessa cosa di una collezione vuota e dovresti scegliere quale rappresenta meglio ciò che stai restituendo. Nella maggior parte dei casi null
è nulla (tranne in SQL). Una collezione vuota è qualcosa, anche se qualcosa di vuoto.
Se devi scegliere l'uno o l'altro, direi che dovresti tendere verso una collezione vuota piuttosto che nulla. Ma ci sono momenti in cui una raccolta vuota non è la stessa cosa di un valore nullo.
Pensa sempre a favore dei tuoi clienti (che usano la tua API):
Restituire 'null' molto spesso crea problemi con i client che non gestiscono correttamente i controlli null, causando una NullPointerException durante il runtime. Ho visto casi in cui un tale controllo null mancante ha forzato un problema di produzione prioritaria (un client utilizzato foreach (...) su un valore null). Durante il test il problema non si è verificato, poiché i dati operati erano leggermente diversi.
Mi piace dare spiegazioni qui, con un esempio adeguato.
Prendi in considerazione un caso qui ...
int totalValue = MySession.ListCustomerAccounts()
.FindAll(ac => ac.AccountHead.AccountHeadID
== accountHead.AccountHeadID)
.Sum(account => account.AccountValue);
Qui considera le funzioni che sto usando ..
1. ListCustomerAccounts() // User Defined
2. FindAll() // Pre-defined Library Function
Posso facilmente usare ListCustomerAccount
e FindAll
invece di.,
int totalValue = 0;
List<CustomerAccounts> custAccounts = ListCustomerAccounts();
if(custAccounts !=null ){
List<CustomerAccounts> custAccountsFiltered =
custAccounts.FindAll(ac => ac.AccountHead.AccountHeadID
== accountHead.AccountHeadID );
if(custAccountsFiltered != null)
totalValue = custAccountsFiltered.Sum(account =>
account.AccountValue).ToString();
}
NOTA: poiché AccountValue non lo è null
, la funzione Sum () non verrà restituita null
. Quindi posso usarla direttamente.
Restituire una raccolta vuota è meglio nella maggior parte dei casi.
Il motivo è la convenienza dell'implementazione del chiamante, un contratto coerente e un'implementazione più semplice.
Se un metodo restituisce null per indicare un risultato vuoto, il chiamante deve implementare un adattatore di controllo null oltre all'enumerazione. Questo codice viene quindi duplicato in vari chiamanti, quindi perché non inserire questo adattatore nel metodo in modo che possa essere riutilizzato.
Un utilizzo valido di null per IEnumerable potrebbe essere un'indicazione di risultato assente o di un fallimento dell'operazione, ma in questo caso dovrebbero essere prese in considerazione altre tecniche, come il lancio di un'eccezione.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
namespace StackOverflow.EmptyCollectionUsageTests.Tests
{
/// <summary>
/// Demonstrates different approaches for empty collection results.
/// </summary>
class Container
{
/// <summary>
/// Elements list.
/// Not initialized to an empty collection here for the purpose of demonstration of usage along with <see cref="Populate"/> method.
/// </summary>
private List<Element> elements;
/// <summary>
/// Gets elements if any
/// </summary>
/// <returns>Returns elements or empty collection.</returns>
public IEnumerable<Element> GetElements()
{
return elements ?? Enumerable.Empty<Element>();
}
/// <summary>
/// Initializes the container with some results, if any.
/// </summary>
public void Populate()
{
elements = new List<Element>();
}
/// <summary>
/// Gets elements. Throws <see cref="InvalidOperationException"/> if not populated.
/// </summary>
/// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>.</returns>
public IEnumerable<Element> GetElementsStrict()
{
if (elements == null)
{
throw new InvalidOperationException("You must call Populate before calling this method.");
}
return elements;
}
/// <summary>
/// Gets elements, empty collection or nothing.
/// </summary>
/// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with zero or more elements, or null in some cases.</returns>
public IEnumerable<Element> GetElementsInconvenientCareless()
{
return elements;
}
/// <summary>
/// Gets elements or nothing.
/// </summary>
/// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with elements, or null in case of empty collection.</returns>
/// <remarks>We are lucky that elements is a List, otherwise enumeration would be needed.</remarks>
public IEnumerable<Element> GetElementsInconvenientCarefull()
{
if (elements == null || elements.Count == 0)
{
return null;
}
return elements;
}
}
class Element
{
}
/// <summary>
/// http://stackoverflow.com/questions/1969993/is-it-better-to-return-null-or-empty-collection/
/// </summary>
class EmptyCollectionTests
{
private Container container;
[SetUp]
public void SetUp()
{
container = new Container();
}
/// <summary>
/// Forgiving contract - caller does not have to implement null check in addition to enumeration.
/// </summary>
[Test]
public void UseGetElements()
{
Assert.AreEqual(0, container.GetElements().Count());
}
/// <summary>
/// Forget to <see cref="Container.Populate"/> and use strict method.
/// </summary>
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void WrongUseOfStrictContract()
{
container.GetElementsStrict().Count();
}
/// <summary>
/// Call <see cref="Container.Populate"/> and use strict method.
/// </summary>
[Test]
public void CorrectUsaOfStrictContract()
{
container.Populate();
Assert.AreEqual(0, container.GetElementsStrict().Count());
}
/// <summary>
/// Inconvenient contract - needs a local variable.
/// </summary>
[Test]
public void CarefulUseOfCarelessMethod()
{
var elements = container.GetElementsInconvenientCareless();
Assert.AreEqual(0, elements == null ? 0 : elements.Count());
}
/// <summary>
/// Inconvenient contract - duplicate call in order to use in context of an single expression.
/// </summary>
[Test]
public void LameCarefulUseOfCarelessMethod()
{
Assert.AreEqual(0, container.GetElementsInconvenientCareless() == null ? 0 : container.GetElementsInconvenientCareless().Count());
}
[Test]
public void LuckyCarelessUseOfCarelessMethod()
{
// INIT
var praySomeoneCalledPopulateBefore = (Action)(()=>container.Populate());
praySomeoneCalledPopulateBefore();
// ACT //ASSERT
Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
}
/// <summary>
/// Excercise <see cref="ArgumentNullException"/> because of null passed to <see cref="Enumerable.Count{TSource}(System.Collections.Generic.IEnumerable{TSource})"/>
/// </summary>
[Test]
[ExpectedException(typeof(ArgumentNullException))]
public void UnfortunateCarelessUseOfCarelessMethod()
{
Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
}
/// <summary>
/// Demonstrates the client code flow relying on returning null for empty collection.
/// Exception is due to <see cref="Enumerable.First{TSource}(System.Collections.Generic.IEnumerable{TSource})"/> on an empty collection.
/// </summary>
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void UnfortunateEducatedUseOfCarelessMethod()
{
container.Populate();
var elements = container.GetElementsInconvenientCareless();
if (elements == null)
{
Assert.Inconclusive();
}
Assert.IsNotNull(elements.First());
}
/// <summary>
/// Demonstrates the client code is bloated a bit, to compensate for implementation 'cleverness'.
/// We can throw away the nullness result, because we don't know if the operation succeeded or not anyway.
/// We are unfortunate to create a new instance of an empty collection.
/// We might have already had one inside the implementation,
/// but it have been discarded then in an effort to return null for empty collection.
/// </summary>
[Test]
public void EducatedUseOfCarefullMethod()
{
Assert.AreEqual(0, (container.GetElementsInconvenientCarefull() ?? Enumerable.Empty<Element>()).Count());
}
}
}
Lo chiamo il mio errore da miliardi di dollari ... A quel tempo, stavo progettando il primo sistema di tipi completo per riferimenti in un linguaggio orientato agli oggetti. Il mio obiettivo era quello di garantire che ogni uso dei riferimenti fosse assolutamente sicuro, con il controllo eseguito automaticamente dal compilatore. Ma non ho resistito alla tentazione di inserire un riferimento null, semplicemente perché era così facile da implementare. Ciò ha portato a innumerevoli errori, vulnerabilità e arresti anomali del sistema, che probabilmente hanno causato un miliardo di dollari di dolore e danni negli ultimi quarant'anni. - Tony Hoare, inventore di ALGOL W.
Vedi qui per un'elaborata tempesta di merda null
in generale. Non sono d'accordo con l'affermazione che undefined
è un'altra null
, ma vale comunque la pena leggere. E spiega, perché dovresti evitare del null
tutto e non solo nel caso che hai chiesto. L'essenza è che null
è in ogni lingua un caso speciale. Devi pensare null
come un'eccezione. undefined
è diverso in questo modo, quel codice che si occupa di comportamenti indefiniti è nella maggior parte dei casi solo un bug. C e la maggior parte delle altre lingue hanno anche un comportamento indefinito, ma la maggior parte di esse non ha un identificatore per questo nella lingua.
Dal punto di vista della gestione della complessità, un obiettivo primario di ingegneria del software, vogliamo evitare la propagazione non necessaria complessità ciclomatiche ai clienti di un'API. Restituire un null al client è come restituirgli il costo della complessità ciclomatica di un altro ramo di codice.
(Ciò corrisponde a un onere di test unitario. Dovresti scrivere un test per il caso di reso nullo, oltre al caso di reso di raccolta vuoto.)