Le funzioni dovrebbero restituire null o un object vuoto?

Qual è la migliore pratica quando si restituiscono dati da funzioni. È meglio restituire un Null o un object vuoto? E perché uno dovrebbe fare uno sull’altro?

Considera questo:

public UserEntity GetUserById(Guid userId) { //Imagine some code here to access database..... //Check if data was returned and return a null if none found if (!DataExists) return null; //Should I be doing this here instead? //return new UserEntity(); else return existingUserEntity; } 

Supponiamo che ci siano casi validi in questo programma che non ci sarebbero informazioni utente nel database con quel GUID. Immagino che non sarebbe appropriato lanciare un’eccezione in questo caso ?? Inoltre ho l’impressione che la gestione delle eccezioni possa danneggiare le prestazioni.

Restituire null è solitamente l’idea migliore se si intende indicare che non sono disponibili dati.

Un object vuoto implica che i dati sono stati restituiti, mentre il rinvio null indica chiaramente che non è stato restituito nulla.

Inoltre, restituendo un valore null si verificherà un’eccezione null se si tenta di accedere ai membri nell’object, il che può essere utile per evidenziare codice buggato: tentare di accedere a un membro di nulla non ha senso. L’accesso ai membri di un object vuoto non fallirà, il che significa che i bug possono non essere scoperti.

Dipende da ciò che ha più senso per il tuo caso.

Ha senso restituire null, ad es. “Non esiste un utente simile”?

O ha senso creare un utente predefinito? Questo ha più senso quando si può tranquillamente presumere che se un utente NON esiste, il codice chiamante vuole che esista quando lo chiedono.

O ha senso lanciare un’eccezione (a la “FileNotFound”) se il codice chiamante richiede un utente con un ID non valido?

Tuttavia, da un punto di vista di separazione delle preoccupazioni / SRP, i primi due sono più corretti. E tecnicamente il primo è il più corretto (ma solo per un capello) – GetUserById dovrebbe essere responsabile solo di una cosa – ottenere l’utente. Gestire il proprio caso “utente inesistente” restituendo qualcos’altro potrebbe essere una violazione di SRP. Separare in un altro controllo – bool DoesUserExist(id) sarebbe appropriato se scegli di lanciare un’eccezione.

Sulla base di numerosi commenti di seguito : se questa è una domanda di progettazione a livello di API, questo metodo potrebbe essere analogo a “OpenFile” o “ReadEntireFile”. Stiamo “aprendo” un utente da qualche repository e idratando l’object dai dati risultanti. Un’eccezione potrebbe essere appropriata in questo caso. Potrebbe non esserlo, ma potrebbe essere.

Tutti gli approcci sono accettabili, dipende solo dal contesto più ampio dell’API / dell’applicazione.

Personalmente, io uso NULL. Si rende chiaro che non ci sono dati da restituire. Ma ci sono casi in cui un object Null può essere utile.

Se il tuo tipo di ritorno è una matrice, restituisci una matrice vuota altrimenti restituisce null.

Dovresti lanciare un’eccezione (solo) se un contratto specifico è rotto.
Nel tuo specifico esempio, richiedendo un UserEntity basato su un ID conosciuto, dipenderebbe dal fatto che gli utenti mancanti (eliminati) siano un caso previsto. In tal caso, restituire null ma se non è un caso previsto, lanciare un’eccezione.
Notare che se la funzione è stata chiamata UserEntity GetUserByName(string name) probabilmente non UserEntity GetUserByName(string name) ma restituirà null. In entrambi i casi restituire un UserEntity vuoto non sarebbe utile.

Per archi, array e raccolte la situazione è solitamente diversa. Ricordo alcuni moduli della linea guida MS che i metodi dovrebbero accettare null come una lista ‘vuota’ ma restituire raccolte di lunghezza zero anziché null . Lo stesso per le stringhe. Si noti che è ansible dichiarare array vuoti: int[] arr = new int[0];

Questa è una domanda aziendale, a seconda che l’esistenza di un utente con un Id Guid specifico sia un caso d’uso normale atteso per questa funzione, o è un’anomalia che impedirà all’applicazione di completare con successo qualsiasi funzione che questo metodo fornisce all’utente object a …

Se si tratta di una “eccezione”, in quanto l’assenza di un utente con quell’Id impedirà all’applicazione di completare con successo qualsiasi funzione svolga, (diciamo che stiamo creando una fattura per un cliente che abbiamo spedito il prodotto a … ), quindi questa situazione dovrebbe generare una ArgumentException (o qualche altra eccezione personalizzata).

Se un utente mancante è a posto, (uno dei possibili esiti normali di chiamare questa funzione) restituisce un null ….

EDIT: (per indirizzare commenti di Adam in un’altra risposta)

Se l’applicazione contiene più processi di business, uno o più dei quali richiedono un utente per essere completato correttamente e uno o più dei quali possono essere completati correttamente senza un utente, allora l’eccezione dovrebbe essere generata più in alto nello stack delle chiamate, più vicino a dove i processi aziendali che richiedono un utente chiamano questo thread di esecuzione. I metodi tra questo metodo e quel punto (dove viene lanciata l’eccezione) devono solo comunicare che non esiste alcun utente (null, booleano, qualunque sia – questo è un dettaglio di implementazione).

Ma se tutti i processi all’interno dell’applicazione richiedono un utente, vorrei comunque lanciare l’eccezione in questo metodo …

Io personalmente restituire null, perché è così che mi aspetto che il livello DAL / Repository agisca.

Se non esiste, non restituire nulla che possa essere interpretato come recuperare con successo un object, null funziona magnificamente qui.

La cosa più importante è essere coerenti sul tuo livello DAL / Repos, in questo modo non ti confondi su come usarlo.

Tendo a

  • return null se l’ID object non esiste quando non è noto in anticipo se deve esistere.
  • throw se l’ID object non esiste quando dovrebbe esistere.

Differenzia questi due scenari con questi tre tipi di metodi. Primo:

 Boolean TryGetSomeObjectById(Int32 id, out SomeObject o) { if (InternalIdExists(id)) { o = InternalGetSomeObject(id); return true; } else { return false; } } 

Secondo:

 SomeObject FindSomeObjectById(Int32 id) { SomeObject o; return TryGetObjectById(id, out o) ? o : null; } 

Terzo:

 SomeObject GetSomeObjectById(Int32 id) { SomeObject o; if (!TryGetObjectById(id, out o)) { throw new SomeAppropriateException(); } return o; } 

Un altro approccio prevede il passaggio di un object di callback o di un delegato che opererà sul valore. Se non viene trovato un valore, il callback non viene chiamato.

 public void GetUserById(Guid id, UserCallback callback) { // Lookup user if (userFound) callback(userEntity); // or callback.Call(userEntity); } 

Funziona bene quando vuoi evitare i controlli nulli su tutto il tuo codice e quando non trovi un valore non è un errore. È anche ansible fornire una richiamata per quando non vengono trovati oggetti se è necessaria un’elaborazione speciale.

 public void GetUserById(Guid id, UserCallback callback, NotFoundCallback notFound) { // Lookup user if (userFound) callback(userEntity); // or callback.Call(userEntity); else notFound(); // or notFound.Call(); } 

Lo stesso approccio usando un singolo object potrebbe essere simile a:

 public void GetUserById(Guid id, UserCallback callback) { // Lookup user if (userFound) callback.Found(userEntity); else callback.NotFound(); } 

Dal punto di vista del design, mi piace molto questo approccio, ma ha lo svantaggio di rendere il sito di chiamata più ingombrante in lingue che non supportano facilmente le funzioni di prima class.

Usiamo CSLA.NET e ci rendiamo conto che un recupero dati fallito dovrebbe restituire un object “vuoto”. Questo è in realtà piuttosto fastidioso, in quanto richiede la convenzione di verificare se obj.IsNew piuttosto che obj == null .

Come menzionato un precedente manifesto, valori di ritorno nulli causeranno il fallimento immediato del codice, riducendo la probabilità di problemi nascosti causati da oggetti vuoti.

Personalmente, penso che null sia più elegante.

È un caso molto comune, e sono sorpreso che la gente qui appaia sorpresa: su qualsiasi applicazione web, i dati vengono spesso recuperati usando un parametro querystring, che può ovviamente essere storpiato, quindi richiede che lo sviluppatore gestisca le incidenze di “non trovato” “.

Puoi gestirlo da:

 if (User.Exists (id)) {
   this.User = User.Fetch (id);
 } altro {
   Response.Redirect ( "~ / notfound.aspx");
 }

… ma questa è una chiamata in più al database ogni volta, il che potrebbe essere un problema nelle pagine ad alto traffico. Mentre:

 this.User = User.Fetch (id);

 se (this.User == null) {
   Response.Redirect ( "~ / notfound.aspx");
 }

… richiede solo una chiamata.

Preferisco null , dal momento che è compatibile con l’operatore a coalescenza nulla ( ?? ).

Direi return null invece di un object vuoto.

Ma l’istanza specifica che hai menzionato qui, stai cercando un utente per id utente, che è una specie di chiave per quell’utente, in quel caso probabilmente vorrei lanciare un’eccezione se non viene trovata alcuna istanza di istanza utente .

Questa è la regola che generalmente seguo:

  • Se non viene trovato alcun risultato in una ricerca mediante l’operazione della chiave primaria, lanciare ObjectNotFoundException.
  • Se nessun risultato trovato in una ricerca da nessun altro criterio, restituisce null.
  • Se nessun risultato trovato in un risultato da un criterio non chiave che può restituire un object multiplo restituisce una raccolta vuota.

Varia in base al contesto, ma in genere restituirò null se sto cercando un object particolare (come nel tuo esempio) e restituisco una raccolta vuota se sto cercando un insieme di oggetti, ma non ce ne sono.

Se hai commesso un errore nel codice e restituisci lead nulli alle eccezioni del puntatore nullo, prima lo noti meglio. Se restituisci un object vuoto, il suo utilizzo iniziale potrebbe funzionare, ma potresti ricevere errori in seguito.

Il migliore in questo caso restituisce “null” nel caso in cui non ci sia un tale utente. Rendi anche statico il tuo metodo.

Modificare:

Solitamente metodi come questo sono membri di qualche class “User” e non hanno accesso ai suoi membri di istanza. In questo caso il metodo deve essere statico, altrimenti è necessario creare un’istanza di “Utente” e quindi chiamare il metodo GetUserById che restituirà un’altra istanza “Utente”. D’accordo, questo è confuso. Ma se il metodo GetUserById è membro di qualche class “DatabaseFactory” – nessun problema per lasciarlo come membro di un’istanza.

Io personalmente restituisco un’istanza predefinita dell’object. Il motivo è che mi aspetto che il metodo restituisca zero a molti o zero a uno (a seconda dello scopo del metodo). L’unico motivo per cui si tratterebbe di uno stato di errore di qualsiasi tipo, utilizzando questo approccio, è se il metodo non restituisce alcun object e si è sempre previsto che lo faccia (in termini di ritorno da uno a molti o singolare).

Per quanto riguarda il presupposto che si tratta di una questione di dominio aziendale – io proprio non la vedo da quel lato dell’equazione. La normalizzazione dei tipi di ritorno è una domanda valida per l’architettura dell’applicazione. Per lo meno, è sobject alla standardizzazione nelle pratiche di codifica. Dubito che ci sia un utente aziendale che sta per dire “nello scenario X, basta dare loro un nulla”.

Nei nostri Business Objects abbiamo 2 metodi Get principali:

Per mantenere le cose semplici nel contesto o domande che sarebbero:

 // Returns null if user does not exist public UserEntity GetUserById(Guid userId) { } // Returns a New User if user does not exist public UserEntity GetNewOrExistingUserById(Guid userId) { } 

Il primo metodo viene utilizzato quando si acquisiscono quadro specifiche, il secondo metodo viene utilizzato specificamente quando si aggiungono o si modificano quadro su pagine Web.

Questo ci consente di avere il meglio di entrambi i mondi nel contesto in cui vengono utilizzati.

Sono uno studente di informatica francese, quindi scusami il mio povero inglese. Nelle nostre classi ci viene detto che un tale metodo non dovrebbe mai restituire null, né un object vuoto. Si suppone che l’utente di questo metodo verifichi prima che l’object che sta cercando esista prima di tentare di ottenerlo.

Usando Java, ci viene chiesto di aggiungere un assert exists(object) : "You shouldn't try to access an object that doesn't exist"; all’inizio di qualsiasi metodo che potrebbe restituire null, per esprimere la “precondizione” (non so qual è la parola in inglese).

IMO questo non è davvero facile da usare ma è quello che sto usando, in attesa di qualcosa di meglio.

Se il caso in cui l’utente non viene trovato appare abbastanza spesso, e vuoi affrontarlo in vari modi a seconda delle circostanze (a volte lanciando un’eccezione, a volte sostituendo un utente vuoto) potresti anche usare qualcosa vicino Option di F # o di Haskell Maybe type, che separa esplicitamente il caso ‘nessun valore’ da ‘found something!’. Il codice di accesso al database potrebbe essere simile a questo:

 public Option GetUserById(Guid userId) { //Imagine some code here to access database..... //Check if data was returned and return a null if none found if (!DataExists) return Option.Nothing; else return Option.Just(existingUserEntity); } 

E sii usato in questo modo:

 Option result = GetUserById(...); if (result.IsNothing()) { // deal with it } else { UserEntity value = result.GetValue(); } 

Sfortunatamente, tutti sembrano girare un tipo come questo.

Di solito restituisco null. Fornisce un meccanismo rapido e semplice per rilevare se qualcosa si è incasinato senza generare eccezioni e utilizzando tonnellate di try / catch dappertutto.

Per i tipi di raccolta restituirei una raccolta vuota, per tutti gli altri tipi preferisco utilizzare i pattern NullObject per restituire un object che implementa la stessa interfaccia di quella del tipo restituito. per i dettagli sul modello controlla il testo del link

Usando il modello NullObject questo sarebbe:

 public UserEntity GetUserById(Guid userId) 

{// Immagina un codice qui per accedere al database …..

  //Check if data was returned and return a null if none found if (!DataExists) return new NullUserEntity(); //Should I be doing this here instead? return new UserEntity(); else return existingUserEntity; 

}

 class NullUserEntity: IUserEntity { public string getFirstName(){ return ""; } ...} 

Per mettere quello che gli altri hanno detto in modo pithier …

Le eccezioni sono per circostanze eccezionali

Se questo metodo è puro livello di accesso ai dati, direi che dato un parametro che viene incluso in un’istruzione select, ci si aspetterebbe che non trovassi alcuna riga da cui creare un object, e quindi restituire null sarebbe accettabile in quanto è la logica di accesso ai dati.

D’altra parte, se mi aspettassi che il mio parametro riflettesse una chiave primaria e dovrei recuperare solo una riga, se ne avessi più di una indietro avrei lanciato un’eccezione. 0 è ok per restituire null, 2 no.

Ora, se avessi avuto un codice di accesso che controllava un provider LDAP e poi verificato un DB per ottenere maggiori dettagli e mi aspettavo che questi dovessero essere sincronizzati in ogni momento, potrei lanciare l’eccezione allora. Come altri hanno detto, sono le regole del business.

Ora dirò che è una regola generale . Ci sono momentjs in cui potresti voler rompere questo. Tuttavia, la mia esperienza e gli esperimenti con C # (un sacco di quello) e Java (un po ‘di tutto questo) mi hanno insegnato che è molto più costoso gestire le eccezioni piuttosto che gestire problemi prevedibili attraverso la logica condizionale. Sto parlando con la melodia di 2 o 3 ordini di grandezza più costosi in alcuni casi. Quindi, se è ansible che il tuo codice possa finire in un ciclo, allora consiglierei di restituire null e testarlo.

Perdona il mio codice pseudo-php.

Penso che dipenda davvero dall’uso previsto del risultato.

Se si intende modificare / modificare il valore di ritorno e salvarlo, restituire un object vuoto. In questo modo, puoi utilizzare la stessa funzione per popolare i dati su un object nuovo o esistente.

Diciamo che ho una funzione che prende una chiave primaria e una matrice di dati, riempie la riga con i dati, quindi salva il record risultante nel db. Dato che intendo popolare l’object con i miei dati in entrambi i casi, può essere un enorme vantaggio recuperare un object vuoto dal getter. In questo modo, posso eseguire operazioni identiche in entrambi i casi. Usi il risultato della funzione getter, non importa quale.

Esempio:

 function saveTheRow($prim_key, $data) { $row = getRowByPrimKey($prim_key); // Populate the data here $row->save(); } 

Qui possiamo vedere che la stessa serie di operazioni manipola tutti i record di questo tipo.

Tuttavia, se l’intento ultimo del valore di ritorno è quello di leggere e fare qualcosa con i dati, allora restituirei null. In questo modo, posso determinare molto rapidamente se non sono stati restituiti dati e visualizzare il messaggio appropriato all’utente.

Solitamente, rileverò eccezioni nella mia funzione che recupera i dati (quindi posso registrare i messaggi di errore, ecc …) quindi restituire null direttamente dal catch. In genere non è importante per l’utente finale quale sia il problema, quindi trovo il modo migliore per incapsulare il mio errore di registrazione / elaborazione direttamente nella funzione che ottiene i dati. Se stai mantenendo una base di codice condivisa in qualsiasi azienda di grandi dimensioni, questo è particolarmente utile perché puoi forzare la corretta registrazione / gestione degli errori anche ai programmatori più pigri.

Esempio:

 function displayData($row_id) { // Logging of the error would happen in this function $row = getRow($row_id); if($row === null) { // Handle the error here } // Do stuff here with data } function getRow($row_id) { $row = null; try{ if(!$db->connected()) { throw excpetion("Couldn't Connect"); } $result = $db->query($some_query_using_row_id); if(count($result) == 0 ) { throw new exception("Couldn't find a record!"); } $row = $db->nextRow(); } catch (db_exception) { //Log db conn error, alert admin, etc... return null; // This way I know that null means an error occurred } return $row; } 

Questa è la mia regola generale. Ha funzionato bene finora.

Interessante domanda e penso che non ci sia una risposta “giusta”, dal momento che dipende sempre dalla responsabilità del tuo codice. Il tuo metodo sa se nessun dato trovato è un problema o no? Nella maggior parte dei casi la risposta è “no” ed è per questo che restituire null e lasciare che il chiamante gestisca la situazione è perfetto.

Forse un buon approccio per distinguere i metodi di lancio dai metodi di null-return è trovare una convenzione nella tua squadra: i metodi che dicono di “ottenere” qualcosa dovrebbero generare un’eccezione se non c’è nulla da ottenere. Methods that may return null could be named differently, perhaps “Find…” instead.

If the object returned is something that can be iterated over, I would return an empty object, so that I don’t have to test for null first.

Esempio:

 bool IsAdministrator(User user) { var groupsOfUser = GetGroupsOfUser(user); // This foreach would cause a run time exception if groupsOfUser is null. foreach (var groupOfUser in groupsOfUser) { if (groupOfUser.Name == "Administrators") { return true; } } return false; } 

I like not to return null from any method, but to use Option functional type instead. Methods that can return no result return an empty Option, rather than null.

Also, such methods that can return no result should indicate that through their name. I normally put Try or TryGet or TryFind at the beginning of the method’s name to indicate that it may return an empty result (eg TryFindCustomer, TryLoadFile, etc.).

That lets the caller apply different techniques, like collection pipelining (see Martin Fowler’s Collection Pipeline ) on the result.

Here is another example where returning Option instead of null is used to reduce code complexity: How to Reduce Cyclomatic Complexity: Option Functional Type

More meat to grind: let’s say my DAL returns a NULL for GetPersonByID as advised by some. What should my (rather thin) BLL do if it receives a NULL? Pass that NULL on up and let the end consumer worry about it (in this case, an ASP.Net page)? How about having the BLL throw an exception?

The BLL may be being used by ASP.Net and Win App, or another class library – I think it is unfair to expect the end consumer to intrinsically “know” that the method GetPersonByID returns a null (unless null types are used, I guess).

My take (for what it’s worth) is that my DAL returns NULL if nothing is found. FOR SOME OBJECTS, that’s ok – it could be a 0:many list of things, so not having any things is fine (eg a list of favourite books). In this case, my BLL returns an empty list. For most single entity things (eg user, account, invoice) if I don’t have one, then that’s definitely a problem and a throw a costly exception. However, seeing as retrieving a user by a unique identifier that’s been previously given by the application should always return a user, the exception is a “proper” exception, as in it’s exceptional. The end consumer of the BLL (ASP.Net, f’rinstance) only ever expects things to be hunky-dory, so an Unhandled Exception Handler will be used instead of wrapping every single call to GetPersonByID in a try – catch block.

If there is a glaring problem in my approach, please let me know as I am always keen to learn. As other posters have said, exceptions are costly things, and the “checking first” approach is good, but exceptions should be just that – exceptional.

I’m enjoying this post, lot’s of good suggestions for “it depends” scenarios 🙂

I think functions should not return null, for the health of your code-base. I can think of a few reasons:

There will be a large quantity of guard clauses treating null reference if (f() != null) .

What is null , is it an accepted answer or a problem? Is null a valid state for a specific object? (imagine that you are a client for the code). I mean all reference types can be null, but should they?

Having null hanging around will almost always give a few unexpected NullRef exceptions from time to time as your code-base grows.

There are some solutions, tester-doer pattern or implementing the option type from functional programming.

I am perplexed at the number of answers (all over the web) that say you need two methods: an “IsItThere()” method and a “GetItForMe()” method and so this leads to a race condition. What is wrong with a function that returns null, assigning it to a variable, and checking the variable for Null all in one test? My formsr C code was peppered with

if ( NULL != (variable = function(arguments…)) ) {

So you get the value (or null) in a variable, and the result all at once. Has this idiom been forgotten? Perché?

I agree with most posts here, which tend towards null .

My reasoning is that generating an empty object with non-nullable properties may cause bugs. For example, an entity with an int ID property would have an initial value of ID = 0 , which is an entirely valid value. Should that object, under some circumstance, get saved to database, it would be a bad thing.

For anything with an iterator I would always use the empty collection. Qualcosa di simile a

 foreach (var eachValue in collection ?? new List(0)) 

is code smell in my opinion. Collection properties shouldn’t be null, ever.

An edge case is String . Many people say, String.IsNullOrEmpty isn’t really necessary, but you cannot always distinguish between an empty string and null. Furthermore, some database systems (Oracle) won’t distinguish between them at all ( '' gets stored as DBNULL ), so you’re forced to handle them equally. The reason for that is, most string values either come from user input or from external systems, while neither textboxes nor most exchange formats have different representations for '' and null . So even if the user wants to remove a value, he cannot do anything more than clearing the input control. Also the distinction of nullable and non-nullable nvarchar database fields is more than questionable, if your DBMS is not oracle – a mandatory field that allows '' is weird, your UI would never allow this, so your constraints do not map. So the answer here, in my opinion is, handle them equally, always.

Concerning your question regarding exceptions and performance: If you throw an exception which you cannot handle completely in your program logic, you have to abort, at some point, whatever your program is doing, and ask the user to redo whatever he just did. In that case, the performance penalty of a catch is really the least of your worries – having to ask the user is the elephant in the room (which means re-rendering the whole UI, or sending some HTML through the internet). So if you don’t follow the anti-pattern of ” Program Flow with Exceptions “, don’t bother, just throw one if it makes sense. Even in borderline cases, such as “Validation Exception”, performance is really not an issue, since you have to ask the user again, in any case.

An Asynchronous TryGet Pattern:

For synchronous methods, I believe @Johann Gerell’s answer is the pattern to use in all cases.

However the TryGet pattern with the out parameter does not work with Async methods.

With C# 7’s Tuple Literals you can now do this:

 async Task<(bool success, SomeObject o)> TryGetSomeObjectByIdAsync(Int32 id) { if (InternalIdExists(id)) { o = await InternalGetSomeObjectAsync(id); return (true, o); } else { return (false, default(SomeObject)); } }