Le funzioni devono restituire null o un oggetto vuoto?


209

Qual è la procedura ottimale per la restituzione dei dati dalle funzioni. È meglio restituire un oggetto Nullo o vuoto? E perché uno dovrebbe fare uno sopra l'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 in questo programma ci siano casi validi che non ci sarebbero informazioni sull'utente nel database con quel GUID. Immagino che non sarebbe appropriato gettare un'eccezione in questo caso ?? Inoltre ho l'impressione che la gestione delle eccezioni possa danneggiare le prestazioni.


4
Penso che tu intenda if (!DataExists).
Sarah Vessels,

107
Questa è una domanda architettonica ed è perfettamente appropriata. La domanda del PO è valida indipendentemente dal problema aziendale che sta cercando di risolvere.
Joseph Ferris,

2
A questa domanda è già stata data una risposta sufficiente. Penso che questa sia una domanda molto interessante.
John Scipione,

'getUser ()' dovrebbe restituire null. 'getCurrentUserInfo ()' o 'getCurrentPermissions ()', OTOH, sarebbero domande più rivelatrici - dovrebbero restituire un oggetto di risposta non nullo indipendentemente da chi / o se qualcuno ha effettuato l'accesso.
Thomas W

2
No @Bergi l'altro è un duplicato. Il mio è stato chiesto per primo, in ottobre, l'altro è stato chiesto 3 mesi dopo a dicembre. Inoltre l'altro parla di una collezione leggermente diversa.
settimane

Risposte:


207

La restituzione di null è di solito l'idea migliore se si intende indicare che non sono disponibili dati.

Un oggetto vuoto implica che i dati sono stati restituiti, mentre la restituzione di null indica chiaramente che non è stato restituito nulla.

Inoltre, la restituzione di un valore nullo comporterà un'eccezione nulla se si tenta di accedere ai membri nell'oggetto, il che può essere utile per evidenziare il codice buggy - il tentativo di accedere a un membro del nulla non ha alcun senso. L'accesso ai membri di un oggetto vuoto non fallirà, il che significa che i bug possono non essere scoperti.


21
Dovresti lanciare un'eccezione, non ingoiare il problema e restituire null. Come minimo, è necessario registrarlo e quindi continuare.
Chris Ballance,

130
@ Chris: Non sono d'accordo. Se il codice documenta chiaramente che il valore restituito è null, è perfettamente accettabile restituire null se non viene trovato alcun risultato corrispondente ai tuoi criteri. Lanciare un'eccezione dovrebbe essere la tua ULTIMA scelta, non la PRIMA.
Mike Hofer,

12
@ Chris: Su quale base lo decidi? L'aggiunta della registrazione all'equazione sembra certamente eccessiva. Lascia che il codice utente decida cosa, se non altro, fare nel caso di nessun utente. Come nel mio commento precedente, non c'è assolutamente alcun problema a restituire un valore che è universalmente definito come "nessun dato".
Adam Robinson,

17
Sono un po 'sconcertato dal fatto che uno sviluppatore Microsoft creda che "restituire null" equivale a "ingoiare il problema". Se la memoria serve, ci sono tonnellate di metodi nel Framework in cui vengono restituiti metodi null se non c'è nulla che corrisponda alla richiesta del chiamante. È "ingoiare il problema?"
Mike Hofer,

5
Ultimo ma non meno importante, ci sarebbe bool GetUserById(Guid userId, out UserEntity result)- che preferirei al valore di ritorno "nullo" e che non è così estremo come generare un'eccezione. Permette un bellissimo nullcodice libero if(GetUserById(x,u)) { ... }.
Marcel Jackwerth,

44

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

Ha senso restituire null, ad es. "Nessun utente esiste"?

O ha senso creare un utente predefinito? Questo ha più senso quando puoi tranquillamente presumere che se un utente NON ESISTE, il codice chiamante intende esistere quando lo richiedono.

Oppure ha senso generare un'eccezione (come "FileNotFound") se il codice chiamante richiede un utente con un ID non valido?

Tuttavia - da una separazione di preoccupazioni / punto di vista SRP, i primi due sono più corretti. E tecnicamente il primo è il più corretto (ma solo per un pelo) - GetUserById dovrebbe essere responsabile solo di una cosa: ottenere l'utente. Gestire il proprio caso "l'utente non esiste" restituendo qualcos'altro potrebbe essere una violazione di SRP. Separarsi in un controllo diverso - bool DoesUserExist(id)sarebbe appropriato se si sceglie di generare un'eccezione.

Sulla base di ampi commenti di seguito : se si tratta di una domanda di progettazione a livello di API, questo metodo potrebbe essere analogo a "OpenFile" o "ReadEntireFile". Stiamo "aprendo" un utente da alcuni repository e idratando l'oggetto dai dati risultanti. Un'eccezione potrebbe essere appropriata in questo caso. Potrebbe non essere, ma potrebbe essere.

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


Qualcuno ti ha votato in basso e io ho votato in secondo piano, poiché questa non mi sembra una cattiva risposta; eccetto: non lancerei mai un'eccezione quando non trovo nessun utente in un metodo come quello indicato dal poster. Se la ricerca di nessun utente implica un ID non valido o qualche problema degno di eccezione, ciò dovrebbe accadere più in alto - il metodo di lancio deve sapere di più da dove proviene quell'ID, ecc.
Jacob Mattison,

(Suppongo che il downvote fosse un'obiezione all'idea di gettare un'eccezione in una circostanza come questa.)
Jacob Mattison,

1
Concordato fino al tuo ultimo punto. Non vi è alcuna violazione di SRP restituendo un valore che è universalmente definito come "nessun dato". È come affermare che un database SQL dovrebbe restituire un errore se una clausola where non produce risultati. Mentre un'eccezione è una scelta di progettazione valida (anche se una che mi irriterebbe come consumatore), non è affatto "più corretta" che restituire null. E no, non sono il DV.
Adam Robinson,

@JacobM generiamo eccezioni quando richiediamo un percorso del filesystem che non esiste, non restituisce null, ma non dai database. Quindi chiaramente entrambi sono appropriati, ed è quello a cui sto arrivando - dipende solo.
Rex M,

2
@Charles: stai rispondendo alla domanda "dovrebbe essere generata un'eccezione ad un certo punto", ma la domanda è "se questa funzione genera l'eccezione". La risposta corretta è "forse", non "sì".
Adam Robinson,

30

Personalmente, uso NULL. Si chiarisce che non ci sono dati da restituire. Ma ci sono casi in cui un oggetto null può essere utile.


Sto per aggiungere questo come risposta. Modello NullObjectPattern o caso speciale. Quindi potresti implementarne uno per ogni caso, NoUserEntitiesFound, NullUserEntities ecc.
David Swindells,

27

Se il tipo restituito è un array, restituisce un array vuoto, altrimenti restituisce null.


0 elementi in un elenco sono uguali a quelli non assegnati in questo momento?
AnthonyWJones,

3
0 elementi in un elenco non sono gli stessi di null. Ti permette di usarlo nelle foreachistruzioni e nelle query linq senza preoccuparti NullReferenceException.
Darin Dimitrov,

5
Sono sorpreso che questo non sia stato più votato. Questa mi sembra una linea guida abbastanza ragionevole.
Shashi,

Bene, il contenitore vuoto è solo un'istanza specifica del modello di oggetti null. Quale potrebbe essere appropriato, non possiamo dirlo.
Deduplicatore,

Restituire un array vuoto quando i dati non sono disponibili è semplicemente sbagliato . C'è una differenza tra i dati disponibili e quelli che non contengono articoli e i dati non disponibili. Restituire un array vuoto in entrambi i casi rende impossibile sapere qual è il caso. Farlo solo per poter usare un foreach senza verificare se i dati esistono persino è oltre stupido - il chiamante dovrebbe controllare se i dati esistono e un'eccezione NullReferenceException se il chiamante non controlla è buono perché espone un bug ..
Ripristina Monica il

12

È necessario generare un'eccezione (solo) in caso di violazione di un contratto specifico.
Nel tuo esempio specifico, richiedere un UserEntity basato su un ID noto, dipenderebbe dal fatto se gli utenti mancanti (eliminati) sono un caso previsto. In tal caso, quindi restituire nullma se non è un caso previsto, generare un'eccezione.
Nota che se la funzione fosse chiamata UserEntity GetUserByName(string name)probabilmente non verrebbe lanciata ma restituirebbe null. In entrambi i casi la restituzione di un UserEntity vuoto non sarebbe utile.

Per stringhe, array e raccolte la situazione è generalmente diversa. Ricordo alcune linee guida della SM che i metodi dovrebbero accettare nullcome un elenco "vuoto" ma restituire raccolte di lunghezza zero anziché null. Lo stesso per le stringhe. Si noti che è possibile dichiarare matrici vuote:int[] arr = new int[0];


Sono contento che tu abbia detto che le stringhe sono diverse, poiché Google mi ha mostrato questo quando stavo decidendo se restituire una stringa vuota.
Noumenon,

Stringhe, raccolte e matrici non sono diverse. Se la SM lo dice, la SM è sbagliata. C'è una differenza tra una stringa vuota e null e tra una raccolta vuota e null. In entrambi i casi il primo rappresenta i dati esistenti (di dimensione 0) e il secondo rappresenta la mancanza di dati. In alcuni casi la distinzione è molto importante. Ad esempio, se si cerca una voce in una cache, si desidera conoscere la differenza tra i dati memorizzati nella cache ma che sono vuoti e i dati non memorizzati nella cache, quindi è necessario recuperarli dall'origine dati sottostante, dove potrebbe non essere vuoto.
Ripristina Monica il

1
Sembra che manchi il punto e il contesto. .Wher(p => p.Lastname == "qwerty")dovrebbe restituire una raccolta vuota, no null.
Henk Holterman,

@HenkHolterman Se si è in grado di accedere alla raccolta completa e applicare un filtro che non accetta elementi nella raccolta, una raccolta vuota è il risultato corretto. Ma se la raccolta completa non esiste, una raccolta vuota è estremamente fuorviante: nulla o lancio sarebbe corretto a seconda che la situazione sia normale o eccezionale. Dato che il tuo post non qualificava la situazione di cui stavi parlando (e ora chiarisci che stai parlando della situazione precedente) e poiché l'OP parlava di quest'ultima situazione, non sono d'accordo con te.
Ripristina Monica il

11

Questa è una domanda aziendale, a seconda che l'esistenza di un utente con un ID guida specifico sia un caso d'uso normale previsto per questa funzione o è un'anomalia che impedirà all'applicazione di completare con successo qualunque funzione fornita da questo metodo opporsi a ...

Se si tratta di una "eccezione", in quanto l'assenza di un utente con quell'ID impedirà all'applicazione di completare correttamente qualsiasi funzione stia eseguendo, (supponiamo che stiamo creando una fattura per un cliente a cui abbiamo spedito il prodotto a ... ), quindi questa situazione dovrebbe generare ArgumentException (o qualche altra eccezione personalizzata).

Se un utente mancante è a posto, (uno dei potenziali risultati normali della chiamata a questa funzione) restituisce un valore nullo ....

EDIT: (per rispondere al commento di Adam in un'altra risposta)

Se l'applicazione contiene più processi aziendali, uno o più dei quali richiede un Utente per essere completato correttamente e uno o più dei quali possono essere completati correttamente senza un utente, l'eccezione deve essere sollevata più in alto nello stack di chiamate, più vicino a dove i processi aziendali che richiedono un utente stanno chiamando questo thread di esecuzione. I metodi tra questo metodo e quel punto (in cui viene generata l'eccezione) dovrebbero semplicemente comunicare che non esiste alcun utente (null, booleano, qualunque cosa - 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 ...


-1 per il downvoter, +1 per Charles - è interamente una questione commerciale e non ci sono buone pratiche per questo.
Austin Salonen,

Attraversa i corsi d'acqua. Che si tratti o meno di una "condizione di errore" è determinato dalla logica aziendale. Come gestire questa è una decisione sull'architettura dell'applicazione. La logica aziendale non imporrà che venga restituito un valore null, ma solo che i requisiti siano soddisfatti. Se l'azienda sta decidendo i tipi di restituzione del metodo, sono troppo coinvolti nell'aspetto tecnico dell'implementazione.
Joseph Ferris,

@joseph, il principio alla base della "gestione strutturata delle eccezioni" è che le eccezioni dovrebbero essere lanciate quando i metodi non possono completare qualsiasi funzione siano state codificate per implementare. Hai ragione, nel senso che se la funzione aziendale è stata codificata per implementare questo metodo può essere "completata con successo", (qualunque cosa ciò significhi nel modello di dominio), non è necessario generare un'eccezione, è possibile restituire un valore nullo , o una variabile "FoundUser" booleana, o qualsiasi altra cosa ... Il modo in cui comunichi al metodo chiamante che nessun utente è stato trovato diventa un dettaglio di implementazione tecnica.
Charles Bretana,

10

Personalmente restituirei null, perché è così che mi aspetto che agisca il livello DAL / Repository.

Se non esiste, non restituire nulla che possa essere interpretato come il recupero corretto di un oggetto, nullqui funziona magnificamente.

La cosa più importante è essere coerenti nel tuo livello DAL / Repos, in modo da non confonderti su come usarlo.


7

Tendo a

  • return nullse l'id oggetto non esiste quando non è noto in anticipo se dovrebbe esistere.
  • throwse l'id oggetto non esiste quando dovrebbe esistere.

Distinguo 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;
}

Intendi di outnoref
Matt Ellen,

@Matt: Sì, signore, sicuramente lo faccio! Fisso.
Johann Gerell,

2
Davvero, questa è l'unica risposta valida per tutti, e quindi la verità totalmente ed esclusivamente! :) Sì, dipende dai presupposti in base ai quali viene invocato il metodo ... Quindi, prima chiarisci questi presupposti e poi scegli la giusta combinazione di quanto sopra. Ho dovuto scorrere troppo verso il basso per arrivare qui :) +100
yair

Questo sembra essere il modello da usare, tranne per il fatto che non supporta i metodi asincroni. Ho fatto riferimento a questa risposta e ho aggiunto una soluzione asincrona con Tuple Literals -> qui
ttugates,

6

Ancora un altro approccio prevede il passaggio di un oggetto 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 si desidera evitare controlli null in tutto il codice e quando non si trova un valore non è un errore. È inoltre possibile fornire una richiamata per quando non viene trovato alcun oggetto 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 che utilizza un singolo oggetto potrebbe apparire come:

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ù voluminoso in lingue che non supportano prontamente le funzioni di prima classe.


Interessante. Quando hai iniziato a parlare di delegati, ho subito iniziato a chiedermi se le espressioni Lambda potessero essere utilizzate qui.
settimane

Sì! A quanto ho capito, la sintassi lambda di C # 3.0 e successive è fondamentalmente zucchero sintattico per delegati anonimi. Allo stesso modo in Java, senza la bella lambda o la sintassi del delegato anonimo, puoi semplicemente creare una classe anonima. È un po 'più brutto, ma può essere davvero utile. Suppongo che in questi giorni, il mio esempio in C # avrebbe potuto usare Func <UserEntity> o qualcosa del genere invece di un delegato nominato, ma l'ultimo progetto C # in cui mi trovavo stava ancora usando la versione 2.
Marc

+1 Mi piace questo approccio. Un problema però è che non è convenzionale e aumenta leggermente la barriera all'ingresso per una base di codice.
Timoxley,

4

Usiamo CSLA.NET e ritiene che un recupero di dati non riuscito dovrebbe restituire un oggetto "vuoto". Questo è in realtà abbastanza fastidioso, in quanto richiede la convenzione di controllare se obj.IsNewpiuttosto che obj == null.

Come menzionato in un precedente poster, i valori di ritorno null causeranno il fallimento immediato del codice, riducendo la probabilità di problemi invisibili causati da oggetti vuoti.

Personalmente, penso che nullsia più elegante.

È un caso molto comune e sono sorpreso che le persone qui sembrino sorprese da esso: su qualsiasi applicazione web, i dati vengono spesso recuperati utilizzando un parametro di querystring, che può ovviamente essere alterato, quindi è necessario che lo sviluppatore gestisca le incidenze di "non trovato ".

Puoi gestirlo tramite:

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

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

this.User = User.Fetch (id);

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

... richiede solo una chiamata.


4

Preferisco null, poiché è compatibile con l'operatore null-coalescing ( ??).


4

Direi return null invece di un oggetto 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 dell'istanza utente .

Questa è la regola che seguo generalmente:

  • Se non viene trovato alcun risultato su un'operazione di ricerca tramite chiave primaria, genera ObjectNotFoundException.
  • Se nessun risultato trovato su una ricerca con altri criteri, restituire null.
  • Se non viene trovato alcun risultato su una ricerca mediante criteri non chiave che potrebbero restituire più oggetti, restituire una raccolta vuota.

Perché dovresti lanciare un'eccezione in uno di questi casi? A volte gli utenti non esistono nel database e ci aspettiamo che ciò non accada. Non è un comportamento eccezionale.
siride,

3

Varia in base al contesto, ma generalmente restituirò null se cerco un oggetto particolare (come nel tuo esempio) e restituirò una raccolta vuota se cerco un insieme di oggetti ma non ce ne sono.

Se hai commesso un errore nel tuo codice e restituire null porta a eccezioni di puntatore null, allora prima lo capisci meglio è. Se si restituisce un oggetto vuoto, il suo utilizzo iniziale potrebbe funzionare, ma è possibile che vengano visualizzati errori in un secondo momento.


+1 Stavo mettendo in dubbio la stessa logica che stai dicendo qui, motivo per cui ho pubblicato la domanda per vedere quali sarebbero le opinioni degli altri su questo
7wp

3

Il migliore in questo caso restituisce "null" in un caso in cui non esiste tale utente. Rendi anche statico il tuo metodo.

Modificare:

Di solito metodi come questo sono membri di una classe "Utente" e non hanno accesso ai suoi membri di istanza. In questo caso il metodo dovrebbe 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 una classe "DatabaseFactory", non c'è problema a lasciarlo come membro di istanza.


Posso chiederti perché vorrei rendere statico il mio metodo? Cosa succede se desidero utilizzare Iniezione dipendenze?
settimane

Ok ora ho la tua logica. Ma mi attengo al modello di repository e mi piace usare l'iniezione di dipendenza per i miei repository, quindi non posso usare metodi statici. Ma +1 per aver suggerito di restituire null :)
7wp

3

Personalmente restituisco un'istanza predefinita dell'oggetto. 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 sarebbe uno stato di errore di qualsiasi tipo, usando questo approccio, è se il metodo non ha restituito alcun oggetto e si aspettava sempre (in termini di ritorno da uno a molti o singolare).

Per quanto riguarda il presupposto che questa sia una domanda di dominio aziendale - non la vedo proprio da quel lato dell'equazione. La normalizzazione dei tipi restituiti è una domanda valida per l'architettura dell'applicazione. Per lo meno, è soggetto a standardizzazione nelle pratiche di codifica. Dubito che ci sia un utente aziendale che dirà "nello scenario X, basta dare loro un null".


+1 Mi piace la visione alternativa del problema. Quindi sostanzialmente stai dicendo quale approccio scelgo dovrebbe andare bene purché il metodo sia coerente in tutta l'applicazione?
settimane

1
Questa è la mia convinzione. Penso che la coerenza sia estremamente importante. Se fai le cose in più modi in più posti, si introduce un rischio maggiore per i nuovi bug. Abbiamo seguito personalmente l'approccio per oggetto predefinito, perché funziona bene con il modello Essence che utilizziamo nel nostro modello di dominio. Abbiamo un unico metodo di estensione generico che possiamo testare su tutti gli oggetti di dominio per dirci se è popolato o meno, in modo che sappiamo che qualsiasi DO può essere testato con una chiamata di objectname.IsDefault () - evitando direttamente eventuali controlli di uguaglianza .
Joseph Ferris,

3

Nei nostri Business Objects abbiamo 2 metodi Get principali:

Per semplificare le cose nel contesto o in caso di domande 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 ottengono entità specifiche, il secondo metodo viene utilizzato specificamente quando si aggiungono o si modificano entità nelle pagine Web.

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


3

Sono uno studente di informatica francese, quindi scusa il mio povero inglese. Nelle nostre classi ci viene detto che un tale metodo non dovrebbe mai restituire nulla, né un oggetto vuoto. L'utente di questo metodo dovrebbe prima verificare che l'oggetto che sta cercando esista prima di provare a ottenerlo.

Usando Java, ci viene chiesto di aggiungere a 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 "condizione preliminare" (non so quale sia la parola in inglese).

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


1
Grazie per la tua risposta Ma non mi piace l'idea di verificare prima se esiste. Il motivo è che genera una query aggiuntiva per il database. In un'applicazione a cui accedono milioni di persone in un giorno, ciò può provocare una drastica perdita di prestazioni.
settimane dal

1
Un vantaggio è che il controllo dell'esistenza è appropriatamente astratto: if (userExists) è leggermente più leggibile, più vicino al dominio problematico e meno "informatico" rispetto a: if (user == null)
timoxley,

E direi che 'if (x == null)' è un modello vecchio di decenni che se non l'hai mai visto prima, non hai scritto codice per molto tempo (e dovresti abituarti così com'è in milioni di righe di codice). "Computery"? Stiamo parlando dell'accesso al database ...
Lloyd Sargent,

3

Se il caso in cui l'utente non viene trovato si presenta 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 a F # Option o Haskell del Maybetipo , che separa esplicitamente il caso "nessun valore" da "trovato qualcosa!". Il codice di accesso al database potrebbe essere simile al seguente:

public Option<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 Option<UserEntity>.Nothing; 
 else
    return Option.Just(existingUserEntity);
}

Ed essere usato in questo modo:

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

Sfortunatamente, tutti sembrano lanciare un tipo come questo.


2

In genere restituisco null. Fornisce un meccanismo rapido e semplice per rilevare se qualcosa ha rovinato senza gettare eccezioni e usare tonnellate di try / catch dappertutto.


2

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

Utilizzando il modello NullObject questo sarebbe: -

public UserEntity GetUserById(Guid userId)

{// Immagina un po 'di 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 ""; } ...} 

2

Per mettere ciò che gli altri hanno detto in modo più conciso ...

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 potrei non trovare alcuna riga da cui costruire un oggetto, e quindi restituire null sarebbe accettabile come questo è la logica di accesso ai dati.

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

Ora, se avessi un codice di accesso che controllava un provider LDAP, quindi controllavo un DB per ottenere maggiori dettagli e mi aspettavo che quelli dovessero essere sempre sincronizzati, allora potrei lanciare l'eccezione. Come altri hanno detto, sono le regole aziendali.

Ora dirò che è una regola generale . Ci sono momenti in cui potresti voler romperlo. Tuttavia, la mia esperienza e i miei esperimenti con C # (molto) e Java (un po 'di quello) mi hanno insegnato che le prestazioni sono molto più costose per gestire le eccezioni piuttosto che gestire i problemi prevedibili tramite la logica condizionale. Sto parlando della melodia di 2 o 3 ordini di grandezza più costosa in alcuni casi. Quindi, se è possibile che il tuo codice possa finire in un ciclo, allora consiglierei di restituire null e testarlo.


2

Perdona il mio pseudo-php / codice.

Penso che dipenda davvero dall'uso previsto del risultato.

Se si intende modificare / modificare il valore restituito e salvarlo, quindi restituire un oggetto vuoto. In questo modo, è possibile utilizzare la stessa funzione per popolare i dati su un oggetto nuovo o esistente.

Supponiamo che io abbia una funzione che accetta una chiave primaria e una matrice di dati, riempie la riga di dati, quindi salva il record risultante nel db. Dato che ho intenzione di popolare l'oggetto con i miei dati in entrambi i modi, può essere un grande vantaggio recuperare un oggetto 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 finale del valore restituito è leggere e fare qualcosa con i dati, allora restituirei null. In questo modo, posso determinare rapidamente se non sono stati restituiti dati e visualizzare il messaggio appropriato per l'utente.

Di solito, rileverò delle eccezioni nella mia funzione che recupera i dati (in modo da poter registrare i messaggi di errore, ecc ...) e quindi restituire null direttamente dalla cattura. In genere non importa all'utente finale quale sia il problema, quindi trovo meglio incapsulare la registrazione / elaborazione degli errori direttamente nella funzione che ottiene i dati. Se stai mantenendo una base di codice condivisa in una grande azienda, questo è particolarmente utile perché puoi forzare la corretta registrazione / gestione degli errori anche sul programmatore più pigro.

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. Finora ha funzionato bene.


2

Domanda interessante e penso che non ci sia una risposta "giusta", poiché 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 sua situazione è perfetto.

Forse un buon approccio per distinguere i metodi di lancio dai metodi di ritorno null è trovare una convenzione nella tua squadra: i metodi che dicono che "ottengono" qualcosa dovrebbero generare un'eccezione se non c'è nulla da ottenere. I metodi che potrebbero restituire null potrebbero essere nominati diversamente, forse invece "Trova ...".


+1 Mi piace l'idea di utilizzare una convenzione di denominazione uniforme per segnalare al programmatore come utilizzare tale funzione.
settimane dal

1
Improvvisamente riconosco che questo è ciò che LINQ fa: considera First (...) vs. FirstOrDefault (...)
Marc Wittke,

2

Se l'oggetto restituito è qualcosa che può essere ripetuto, restituirei un oggetto vuoto, in modo da non dover prima provare per null.

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;
}

2

Mi piace non restituire null da qualsiasi metodo, ma utilizzare invece il tipo funzionale Opzione. I metodi che non possono restituire alcun risultato restituiscono un'opzione vuota, anziché nulla.

Inoltre, tali metodi che non possono restituire alcun risultato dovrebbero indicarlo tramite il loro nome. Normalmente inserisco Try o TryGet o TryFind all'inizio del nome del metodo per indicare che potrebbe restituire un risultato vuoto (ad esempio TryFindCustomer, TryLoadFile, ecc.).

Ciò consente al chiamante di applicare diverse tecniche, come la pipeline di raccolta (vedere Pipeline di raccolta di Martin Fowler ) sul risultato.

Ecco un altro esempio in cui viene utilizzata l'opzione Return anziché null per ridurre la complessità del codice: Come ridurre la complessità ciclomatica: Opzione Tipo funzionale


1
Ho scritto una risposta, posso vedere che è simile alla tua mentre ho scorrere verso l'alto e sono d'accordo, puoi implementare un tipo di opzione con una raccolta generica con 0 o 1 elementi. Grazie per i collegamenti aggiuntivi.
Gabriel P.

1

Più carne da macinare: diciamo che il mio DAL restituisce un valore NULL per GetPersonByID come consigliato da alcuni. Cosa dovrebbe fare il mio BLL (piuttosto sottile) se riceve un NULL? Trasmetti quel NULL e lascia che il consumatore finale se ne preoccupi (in questo caso, una pagina ASP.Net)? Che ne dici di fare in modo che BLL generi un'eccezione?

Il BLL potrebbe essere utilizzato da ASP.Net e Win App o da un'altra libreria di classi - penso che sia ingiusto aspettarsi che il consumatore finale "sappia" intrinsecamente che il metodo GetPersonByID restituisce un valore null (a meno che non vengano utilizzati tipi null, immagino ).

La mia opinione (per quello che vale) è che il mio DAL restituisce NULL se non viene trovato nulla. PER ALCUNI OGGETTI, va bene - potrebbe essere un elenco di cose 0: molte, quindi non avere nulla va bene (ad esempio un elenco di libri preferiti). In questo caso, il mio BLL restituisce un elenco vuoto. Per la maggior parte delle cose di una singola entità (ad esempio utente, account, fattura) se non ne ho una, questo è sicuramente un problema e un'eccezione costosa. Tuttavia, visto che il recupero di un utente da un identificatore univoco precedentemente fornito dall'applicazione dovrebbe sempre restituire un utente, l'eccezione è un'eccezione "corretta", come nella sua eccezionale. Il consumatore finale del BLL (ASP.Net, f'rinstance) si aspetta sempre che le cose siano hunky-dory, quindi verrà utilizzato un gestore di eccezioni non gestite invece di racchiudere ogni singola chiamata a GetPersonByID in un blocco try-catch.

Se c'è un evidente problema nel mio approccio, per favore fatemi sapere come sono sempre desideroso di imparare. Come hanno affermato altri poster, le eccezioni sono cose costose e l'approccio del "primo controllo" è buono, ma le eccezioni dovrebbero essere proprio queste: eccezionali.

Mi piace questo post, molti buoni suggerimenti per scenari "dipende" :-)


E, naturalmente, oggi mi sono imbattuto in uno scenario in cui restituirò NULL dal mio BLL ;-) Detto questo, potrei ancora lanciare un'eccezione e utilizzare try / catch nella mia classe di consumo MA ho ancora un problema : come fa la mia classe di consumo a usare un try / catch, simile come fanno a sapere se c'è NULL?
Mike Kingscott,

Puoi documentare che un metodo genera un'eccezione tramite il doctag @throws e documenti il ​​fatto che può restituire null nel doctag @return.
Timoxley,

1

Penso che le funzioni non debbano restituire null, per la salute della tua base di codice. Mi vengono in mente alcuni motivi:

Ci sarà una grande quantità di clausole di protezione che trattano il riferimento null if (f() != null).

Cos'è null, è una risposta accettata o un problema? Null è uno stato valido per un oggetto specifico? (immagina di essere un cliente per il codice). Voglio dire, tutti i tipi di riferimento possono essere nulli, ma dovrebbero?

Avere in nullgiro quasi sempre darà alcune eccezioni inattese NullRef di volta in volta man mano che la tua base di codice cresce.

Esistono alcune soluzioni tester-doer patterno implementare la option typeprogrammazione funzionale.


0

Sono perplesso dal numero di risposte (su tutto il Web) che dicono che hai bisogno di due metodi: un metodo "IsItThere ()" e un metodo "GetItForMe ()" e quindi questo porta a una condizione di competizione. Cosa c'è di sbagliato in una funzione che restituisce null, assegnandola a una variabile e controllando la variabile Null tutto in un test? Il mio ex codice C era pieno di pepe

if (NULL! = (variabile = funzione (argomenti ...))) {

Quindi ottieni il valore (o null) in una variabile e il risultato tutto in una volta. Questo idioma è stato dimenticato? Perché?


0

Sono d'accordo con la maggior parte dei post qui, che tendono verso null.

Il mio ragionamento è che la generazione di un oggetto vuoto con proprietà non annullabili può causare bug. Ad esempio, un'entità con una int IDproprietà avrebbe un valore iniziale di ID = 0, che è un valore completamente valido. Se quell'oggetto, in qualche circostanza, venisse salvato nel database, sarebbe una brutta cosa.

Per qualsiasi cosa con un iteratore userei sempre la raccolta vuota. Qualcosa di simile a

foreach (var eachValue in collection ?? new List<Type>(0))

è odore di codice secondo me. Le proprietà della raccolta non dovrebbero mai essere nulle.

Un caso limite è String. Molte persone dicono, String.IsNullOrEmptynon è davvero necessario, ma non è sempre possibile distinguere tra una stringa vuota e nulla. Inoltre, alcuni sistemi di database (Oracle) non li distinguono affatto ( ''vengono archiviati come DBNULL), quindi sei costretto a gestirli equamente. Il motivo è che la maggior parte dei valori di stringa provengono dall'input dell'utente o da sistemi esterni, mentre né le caselle di testo né la maggior parte dei formati di scambio hanno rappresentazioni diverse per ''e null. Pertanto, anche se l'utente desidera rimuovere un valore, non può fare altro che cancellare il controllo di input. Anche la distinzione tra nullable e non nullablenvarchar campi di database è più che discutibile, se il tuo DBMS non è un oracolo - un campo obbligatorio che consente''è strano, la tua UI non lo permetterebbe mai, quindi i tuoi vincoli non vengono mappati. Quindi la risposta qui, a mio avviso, è gestirli equamente, sempre.

Per quanto riguarda la domanda relativa alle eccezioni e alle prestazioni: se si genera un'eccezione che non è possibile gestire completamente nella logica del programma, è necessario interrompere, a un certo punto, qualunque cosa il programma stia facendo e chiedere all'utente di ripetere tutto ciò che ha appena fatto. In tal caso, la penalità prestazionale di a catchè davvero l'ultima delle tue preoccupazioni: dover chiedere all'utente è l'elefante nella stanza (il che significa ridistribuire l'intera interfaccia utente o inviare un codice HTML attraverso Internet). Quindi, se non segui l'anti-schema di " Flusso del programma con eccezioni ", non preoccuparti, lanciane uno se ha senso. Anche in casi limite, come "Eccezione di convalida", le prestazioni non sono in realtà un problema, poiché in ogni caso è necessario chiedere nuovamente all'utente.


0

Un modello asincrono TryGet:

Per i metodi sincroni, credo che la risposta di @Johann Gerell sia il modello da utilizzare in tutti i casi.

Tuttavia, il modello TryGet con il outparametro non funziona con i metodi Async.

Con Tuple Literals di C # 7 ora puoi fare questo:

async Task<(bool success, SomeObject o)> TryGetSomeObjectByIdAsync(Int32 id)
{
    if (InternalIdExists(id))
    {
        o = await InternalGetSomeObjectAsync(id);

        return (true, o);
    }
    else
    {
        return (false, default(SomeObject));
    }
}
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.