Les fonctions doivent-elles retourner null ou un objet vide?


209

Quelle est la meilleure pratique lors du renvoi de données à partir de fonctions. Est-il préférable de retourner un Null ou un objet vide? Et pourquoi l'un devrait-il faire l'un sur l'autre?

Considère ceci:

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

Permet de prétendre qu'il y aurait des cas valides dans ce programme qu'il n'y aurait aucune information utilisateur dans la base de données avec ce GUID. J'imagine qu'il ne serait pas approprié de lever une exception dans ce cas ?? J'ai également l'impression que la gestion des exceptions peut nuire aux performances.


4
Je pense que tu veux dire if (!DataExists).
Sarah Vessels

107
Il s'agit d'une question architecturale et parfaitement appropriée. La question du PO est valable quel que soit le problème commercial qu'il essaie de résoudre.
Joseph Ferris

2
Cette question a déjà reçu une réponse suffisante. Je pense que c'est une question très intéressante.
John Scipione

'getUser ()' devrait retourner null. 'getCurrentUserInfo ()' ou 'getCurrentPermissions ()', OTOH, seraient des questions plus révélatrices - ils devraient renvoyer un objet de réponse non nul, peu importe qui / ou si quelqu'un est connecté.
Thomas W

2
Non @Bergi l'autre est un doublon. La mienne a été posée en premier, en octobre, l'autre en 3 papillons plus tard en décembre. De plus, l'autre parle d'une collection légèrement différente.
7wp

Réponses:


207

Renvoyer null est généralement la meilleure idée si vous avez l'intention d'indiquer qu'aucune donnée n'est disponible.

Un objet vide implique que des données ont été retournées, alors que le retour de null indique clairement que rien n'a été retourné.

En outre, le retour d'une valeur Null entraînera une exception Null si vous tentez d'accéder aux membres de l'objet, ce qui peut être utile pour mettre en évidence le code bogué - tenter d'accéder à un membre de rien n'a aucun sens. L'accès aux membres d'un objet vide n'échouera pas, ce qui signifie que les bogues peuvent rester inconnus.


21
Vous devez lever une exception, ne pas avaler le problème et renvoyer null. Au minimum, vous devez vous connecter et continuer.
Chris Ballance

130
@Chris: Je ne suis pas d'accord. Si le code indique clairement que la valeur de retour est nulle, il est parfaitement acceptable de retourner la valeur null si aucun résultat ne correspond à vos critères. Lancer une exception devrait être votre DERNIER choix, pas votre PREMIER.
Mike Hofer

12
@Chris: Sur quelle base décidez-vous cela? L'ajout de la journalisation à l'équation semble certainement excessif. Laissez le code consommateur décider ce qui, le cas échéant, doit être fait en cas d'absence d'utilisateur. Comme pour mon commentaire précédent, il n'y a absolument aucun problème avec le retour d'une valeur qui est universellement définie comme "aucune donnée".
Adam Robinson,

17
Je suis un peu déconcerté qu'un développeur Microsoft pense que «retourner null» équivaut à «avaler le problème». Si la mémoire est bonne, il y a des tonnes de méthodes dans le Framework où les méthodes null sont retournées s'il n'y a rien qui correspond à la demande de l'appelant. Est-ce "avaler le problème?"
Mike Hofer

5
Enfin et surtout, il y en aurait bool GetUserById(Guid userId, out UserEntity result)- que je préférerais à la valeur de retour "nulle", et qui n'est pas aussi extrême que de lever une exception. Il permet un beau nullcode comme if(GetUserById(x,u)) { ... }.
Marcel Jackwerth

44

Cela dépend de ce qui a le plus de sens pour votre cas.

Est-il sensé de retourner null, par exemple "aucun utilisateur n'existe"?

Ou est-il judicieux de créer un utilisateur par défaut? Cela est plus logique lorsque vous pouvez supposer en toute sécurité que si un utilisateur N'EXISTE PAS, le code appelant a l'intention qu'un tel existe lorsqu'il le demande.

Ou est-il judicieux de lever une exception (à la "FileNotFound") si le code appelant exige un utilisateur avec un ID non valide?

Cependant - du point de vue de la séparation des préoccupations et du PÉR, les deux premiers sont plus corrects. Et techniquement, le premier est le plus correct (mais seulement par un cheveu) - GetUserById ne devrait être responsable que d'une chose - obtenir l'utilisateur. Gérer son propre cas "utilisateur n'existe pas" en renvoyant quelque chose d'autre pourrait être une violation de SRP. Séparer en un chèque différent - bool DoesUserExist(id)serait approprié si vous choisissez de lever une exception.

Sur la base de nombreux commentaires ci - dessous : s'il s'agit d'une question de conception au niveau de l'API, cette méthode pourrait être analogue à "OpenFile" ou "ReadEntireFile". Nous «ouvrons» un utilisateur à partir d'un référentiel et hydratons l'objet à partir des données résultantes. Une exception pourrait être appropriée dans ce cas. Peut-être pas, mais ça pourrait l'être.

Toutes les approches sont acceptables - cela dépend simplement, en fonction du contexte plus large de l'API / de l'application.


Quelqu'un vous a rejeté et je vous ai voté en arrière, car cela ne me semble pas être une mauvaise réponse; sauf: je ne lèverais jamais d'exception en ne trouvant aucun utilisateur dans une méthode comme celle donnée par l'affiche. Si le fait de ne trouver aucun utilisateur implique un ID invalide ou un problème de ce type, cela devrait se produire plus haut - la méthode de lancement doit en savoir plus sur la provenance de cet ID, etc.
Jacob Mattison

(Je suppose que le downvote était une objection à l'idée de lever une exception dans une circonstance comme celle-ci.)
Jacob Mattison

1
D'accord jusqu'à votre dernier point. Il n'y a aucune violation de SRP en renvoyant une valeur qui est universellement définie comme "aucune donnée". Cela revient à dire qu'une base de données SQL doit renvoyer une erreur si une clause where ne produit aucun résultat. Bien qu'une exception soit un choix de conception valide (même si cela m'irriterait en tant que consommateur), elle n'est pas "plus correcte" que de renvoyer null. Et non, je ne suis pas le DV.
Adam Robinson,

@JacobM nous lançons des exceptions lorsque nous demandons un chemin de système de fichiers qui n'existe pas, ne renvoie pas null, mais pas à partir de bases de données. Il est donc clair que les deux sont appropriés, ce à quoi je veux en venir - cela dépend simplement.
Rex M

2
@Charles: Vous répondez à la question "si une exception est levée à un moment donné", mais la question est "si cette fonction lève l'exception". La bonne réponse est "peut-être", pas "oui".
Adam Robinson,

30

Personnellement, j'utilise NULL. Il indique clairement qu'il n'y a aucune donnée à renvoyer. Mais il y a des cas où un objet nul peut être utile.


Je suis sur le point d'ajouter moi-même cette réponse. NullObjectPattern ou motif de cas spécial. Ensuite, vous pouvez implémenter un pour chaque cas, NoUserEntitiesFound, NullUserEntities etc.
David Swindells

27

Si votre type de retour est un tableau, retournez un tableau vide sinon retournez null.


Est-ce que 0 élément dans une liste est identique à la liste non affectée en ce moment?
AnthonyWJones

3
0 élément dans une liste n'est pas le même que null. Il vous permet de l'utiliser foreachsans souci pour les instructions et les requêtes linq NullReferenceException.
Darin Dimitrov

5
Je suis surpris que cela n'ait pas été davantage voté. Cela me semble être une directive assez raisonnable.
shashi

Eh bien, un conteneur vide n'est qu'une instance spécifique du modèle d'objet nul. Ce qui pourrait être approprié, nous ne pouvons pas le dire.
Deduplicator

Renvoyer un tableau vide lorsque les données ne sont pas disponibles est tout simplement faux . Il y a une différence entre les données disponibles et ne contenant aucun élément et les données non disponibles. Le retour d'un tableau vide dans les deux cas rend impossible de savoir quel est le cas. Le faire juste pour pouvoir utiliser un foreach sans vérifier si les données existent est au-delà de la bêtise - l'appelant devrait avoir à vérifier si les données existent et une NullReferenceException si l'appelant ne vérifie pas est bonne car elle expose un bogue ..
Rétablir Monica

12

Vous devez lever une exception (uniquement) si un contrat spécifique est rompu.
Dans votre exemple spécifique, demander une UserEntity basée sur un ID connu, cela dépendra du fait si des utilisateurs manquants (supprimés) sont un cas attendu. Si c'est le cas, retournez-le, nullmais si ce n'est pas un cas attendu, lancez une exception.
Notez que si la fonction était appelée, UserEntity GetUserByName(string name)elle ne lancerait probablement pas mais retournerait null. Dans les deux cas, renvoyer une UserEntity vide serait inutile.

Pour les chaînes, les tableaux et les collections, la situation est généralement différente. Je me souviens d'une forme de ligne directrice MS que les méthodes devraient accepter nullcomme une liste «vide» mais renvoyer des collections de longueur nulle plutôt que null. De même pour les cordes. Notez que vous pouvez déclarer des tableaux vides:int[] arr = new int[0];


Heureux que vous ayez mentionné que les chaînes sont différentes, car Google me l'a montré lorsque je décidais de renvoyer une chaîne vide.
Noumenon

Les chaînes, les collections et les tableaux ne sont pas différents. Si MS le dit, MS a tort. Il existe une différence entre une chaîne vide et null, et entre une collection vide et null. Dans les deux cas, le premier représente les données existantes (de taille 0) et le second représente le manque de données. Dans certains cas, la distinction est très importante. Par exemple, si vous recherchez une entrée dans un cache, vous souhaitez connaître la différence entre les données mises en cache mais vides et les données non mises en cache afin de les extraire de la source de données sous-jacente, où elles pourraient ne pas être vide.
Rétablir Monica

1
Vous semblez manquer le point et le contexte. .Wher(p => p.Lastname == "qwerty")devrait retourner une collection vide, non null.
Henk Holterman

@HenkHolterman Si vous pouvez accéder à la collection complète et appliquer un filtre qui n'accepte aucun élément de la collection, une collection vide est le résultat correct. Mais si la collection complète n'existe pas, une collection vide est extrêmement trompeuse - null ou throw serait correct selon que la situation est normale ou exceptionnelle. Comme votre poste ne qualifiait pas la situation dont vous parliez (et maintenant vous clarifiez que vous parlez de la première situation) et que le PO parlait de la dernière situation, je dois être en désaccord avec vous.
Rétablir Monica

11

Il s'agit d'une question commerciale, selon que l'existence d'un utilisateur avec un identifiant de guidage spécifique est un cas d'utilisation normal attendu pour cette fonction, ou est-ce une anomalie qui empêchera l'application de remplir avec succès la fonction que cette méthode fournit à l'utilisateur s'opposer à ...

S'il s'agit d'une "exception", dans la mesure où l'absence d'un utilisateur avec cet ID empêchera l'application de remplir avec succès la fonction qu'elle exécute (supposons que nous créons une facture pour un client auquel nous avons expédié le produit ... ), cette situation doit alors lever une ArgumentException (ou une autre exception personnalisée).

Si un utilisateur manquant est ok, (l'un des résultats normaux potentiels de l'appel de cette fonction), retournez un null

EDIT: (pour répondre au commentaire d'Adam dans une autre réponse)

Si l'application contient plusieurs processus métier, dont un ou plusieurs nécessitent un utilisateur pour se terminer avec succès, et dont un ou plusieurs peuvent se terminer sans utilisateur, alors l'exception doit être levée plus haut dans la pile des appels, plus près de l'endroit où les processus métier qui nécessitent un utilisateur appellent ce fil d'exécution. Les méthodes entre cette méthode et ce point (où l'exception est levée) devraient simplement communiquer qu'aucun utilisateur n'existe (null, booléen, peu importe - c'est un détail d'implémentation).

Mais si tous les processus de l'application nécessitent un utilisateur, je lèverais quand même l'exception dans cette méthode ...


-1 à l'électeur descendant, +1 à Charles - c'est entièrement une question commerciale et il n'y a pas de meilleure pratique pour cela.
Austin Salonen,

Il traverse les ruisseaux. Qu'il s'agisse ou non d'une «condition d'erreur» est déterminé par la logique métier. Comment gérer cela est une décision d'architecture d'application. La logique métier ne dictera pas le retour d'une valeur nulle, juste que les exigences sont satisfaites. Si l'entreprise décide des types de retour de méthode, elle est trop impliquée dans l'aspect technique de la mise en œuvre.
Joseph Ferris

@joseph, le principe de base de la "gestion structurée des exceptions" est que les exceptions doivent être levées lorsque les méthodes ne peuvent pas exécuter la fonction pour laquelle elles ont été codées. Vous avez raison, en ce sens que si la fonction métier que cette méthode a été codée pour être mise en œuvre peut être "terminée avec succès", (quoi que cela signifie dans le modèle de domaine), alors vous n'avez pas besoin de lever d'exception, vous pouvez retourner une valeur nulle , ou une variable booléenne "FoundUser", ou autre chose ... Comment vous communiquez à la méthode d'appel qu'aucun utilisateur n'a été trouvé devient alors un détail d'implémentation technique.
Charles Bretana,

10

Personnellement, je retournerais null, car c'est ainsi que je m'attendrais à ce que la couche DAL / Repository agisse.

S'il n'existe pas, ne renvoyez rien qui pourrait être interprété comme ayant réussi à récupérer un objet, nullfonctionne à merveille ici.

La chose la plus importante est d'être cohérent dans votre couche DAL / Repos, de cette façon vous ne vous trompez pas sur la façon de l'utiliser.


7

J'ai tendance à

  • return nullsi l'ID d'objet n'existe pas alors qu'on ne sait pas au préalable s'il doit exister.
  • throwsi l'ID d'objet n'existe pas alors qu'il devrait exister.

Je différencie ces deux scénarios avec ces trois types de méthodes. Première:

Boolean TryGetSomeObjectById(Int32 id, out SomeObject o)
{
    if (InternalIdExists(id))
    {
        o = InternalGetSomeObject(id);

        return true;
    }
    else
    {
        return false;
    }
}

Seconde:

SomeObject FindSomeObjectById(Int32 id)
{
    SomeObject o;

    return TryGetObjectById(id, out o) ? o : null;
}

Troisième:

SomeObject GetSomeObjectById(Int32 id)
{
    SomeObject o;

    if (!TryGetObjectById(id, out o))
    {
        throw new SomeAppropriateException();
    }

    return o;
}

Tu veux dire outpasref
Matt Ellen

@Matt: Oui, monsieur, je le fais très certainement! Fixé.
Johann Gerell

2
Vraiment, c'est la seule réponse universelle, et donc la vérité entière et unique! :) Oui, cela dépend des hypothèses sur la base desquelles la méthode est invoquée ... Donc, clarifiez d'abord ces hypothèses, puis choisissez la bonne combinaison de ce qui précède. J'ai dû faire défiler trop vers le bas pour arriver ici :) +100
yair

Cela semble être le modèle à utiliser, sauf qu'il ne prend pas en charge les méthodes asynchrones. J'ai référencé cette réponse et ajouté une solution asynchrone avec
Tuple Literals

6

Une autre approche consiste à passer un objet de rappel ou un délégué qui opérera sur la valeur. Si aucune valeur n'est trouvée, le rappel n'est pas appelé.

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

Cela fonctionne bien lorsque vous voulez éviter les vérifications nulles partout dans votre code, et lorsque vous ne trouvez pas de valeur, ce n'est pas une erreur. Vous pouvez également fournir un rappel lorsqu'aucun objet n'est trouvé si vous avez besoin d'un traitement spécial.

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

La même approche utilisant un seul objet pourrait ressembler à ceci:

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

Du point de vue de la conception, j'aime vraiment cette approche, mais elle a l'inconvénient de rendre le site d'appel plus volumineux dans les langues qui ne prennent pas facilement en charge les fonctions de première classe.


Intéressant. Lorsque vous avez commencé à parler des délégués, j'ai immédiatement commencé à me demander si les expressions Lambda pouvaient être utilisées ici.
7wp

Ouaip! Si je comprends bien, la syntaxe lambda de C # 3.0 et plus est fondamentalement du sucre syntaxique pour les délégués anonymes. De même en Java, sans la jolie syntaxe lambda ou délégué anonyme, vous pouvez simplement créer une classe anonyme. C'est un peu plus laid, mais cela peut être très pratique. Je suppose que ces jours -ci , mon exemple C # aurait pu utiliser Func <UserEntity> ou quelque chose comme ça au lieu d'un délégué nommé, mais le dernier projet C # J'étais utilisait toujours la version 2.
Marc

+1 J'aime cette approche. Un problème est cependant qu'il n'est pas conventionnel et augmente légèrement la barrière à l'entrée pour une base de code.
timoxley

4

Nous utilisons CSLA.NET et il considère qu'une extraction de données ayant échoué doit renvoyer un objet "vide". C'est en fait assez ennuyeux, car il exige la convention de vérifier si obj.IsNewplutôt que obj == null.

Comme mentionné dans une affiche précédente, les valeurs de retour nulles entraîneront immédiatement l'échec du code, réduisant ainsi la probabilité de problèmes de furtivité causés par des objets vides.

Personnellement, je pense que nullc'est plus élégant.

C'est un cas très courant, et je suis surpris que les gens ici semblent surpris: sur n'importe quelle application Web, les données sont souvent récupérées à l'aide d'un paramètre de chaîne de requête, qui peut évidemment être modifié, ce qui oblige le développeur à gérer les incidences de "non trouvé". ".

Vous pouvez gérer cela en:

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

... mais c'est un appel supplémentaire à la base de données à chaque fois, ce qui peut être un problème sur les pages à fort trafic. Tandis que:

this.User = User.Fetch (id);

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

... ne nécessite qu'un seul appel.


4

Je préfère null, car il est compatible avec l'opérateur de coalescence nulle ( ??).


4

Je dirais retourner null au lieu d'un objet vide.

Mais l'instance spécifique que vous avez mentionnée ici, vous recherchez un utilisateur par ID utilisateur, qui est en quelque sorte la clé de cet utilisateur, dans ce cas, je voudrais probablement lever une exception si aucune instance d'instance d'utilisateur n'est trouvée. .

C'est la règle que je respecte généralement:

  • Si aucun résultat n'est trouvé sur une opération de recherche par clé primaire, lancez ObjectNotFoundException.
  • Si aucun résultat trouvé sur une recherche par d'autres critères, retourne null.
  • Si aucun résultat trouvé sur une recherche par un critère non clé qui peut renvoyer plusieurs objets renvoie une collection vide.

Pourquoi voudriez-vous lever une exception dans l'un de ces cas? Parfois, les utilisateurs n'existent pas dans la base de données, et nous nous attendons à ce que cela n'arrive pas. Ce n'est pas un comportement exceptionnel.
siride

3

Cela variera en fonction du contexte, mais je retournerai généralement null si je recherche un objet particulier (comme dans votre exemple) et retournerai une collection vide si je cherche un ensemble d'objets mais il n'y en a pas.

Si vous avez fait une erreur dans votre code et que le retour de null conduit à des exceptions de pointeur nul, alors le plus tôt sera le mieux. Si vous renvoyez un objet vide, son utilisation initiale peut fonctionner, mais vous pouvez obtenir des erreurs ultérieurement.


+1 Je remettais en question la même logique que vous dites ici, c'est pourquoi j'ai posté la question pour voir quelles seraient les opinions des autres à ce sujet
7wp

3

Le meilleur dans ce cas renvoie "null" dans un cas où il n'y a pas un tel utilisateur. Rendez également votre méthode statique.

Éditer:

Habituellement, des méthodes comme celle-ci sont membres d'une classe "Utilisateur" et n'ont pas accès à ses membres d'instance. Dans ce cas, la méthode doit être statique, sinon vous devez créer une instance de "User" puis appeler la méthode GetUserById qui retournera une autre instance de "User". D'accord, c'est déroutant. Mais si la méthode GetUserById est membre d'une classe "DatabaseFactory" - pas de problème pour la laisser comme membre d'instance.


Puis-je demander pourquoi je voudrais rendre ma méthode statique? Et si je veux utiliser l'injection de dépendance?
7wp

Ok maintenant je comprends ta logique. Mais je m'en tiens au modèle de référentiel, et j'aime utiliser l'injection de dépendance pour mes référentiels, donc je ne peux pas utiliser de méthodes statiques. Mais +1 pour avoir suggéré de retourner null :)
7wp

3

Je retourne personnellement une instance par défaut de l'objet. La raison en est que je m'attends à ce que la méthode retourne zéro à plusieurs ou zéro à un (selon le but de la méthode). La seule raison pour laquelle il s'agirait d'un état d'erreur de quelque nature que ce soit, en utilisant cette approche, est que la méthode n'a renvoyé aucun objet (s) et était toujours attendue (en termes de retour un à plusieurs ou singulier).

En ce qui concerne l'hypothèse qu'il s'agit d'une question de domaine commercial - je ne la vois tout simplement pas de ce côté de l'équation. La normalisation des types de retour est une question d'architecture d'application valide. À tout le moins, il est sujet à normalisation dans les pratiques de codage. Je doute qu'il y ait un utilisateur professionnel qui va dire "dans le scénario X, donnez-lui simplement une valeur nulle".


+1 J'aime la vue alternative du problème. Donc, fondamentalement, vous dites que l'approche que je choisis devrait convenir tant que la méthode est cohérente tout au long de l'application?
7wp

1
Telle est ma conviction. Je pense que la cohérence est extrêmement importante. Si vous faites les choses de plusieurs façons à plusieurs endroits, cela présente un risque plus élevé de nouveaux bogues. Nous avons personnellement opté pour l'approche par défaut des objets, car elle fonctionne bien avec le modèle Essence que nous utilisons dans notre modèle de domaine. Nous avons une seule méthode d'extension générique que nous pouvons tester contre tous les objets de domaine pour nous dire si elle est remplie ou non, afin que nous sachions que n'importe quel DO peut être testé avec un appel de objectname.IsDefault () - en évitant tout contrôle d'égalité directement .
Joseph Ferris

3

Dans nos objets métier, nous avons 2 principales méthodes Get:

Pour garder les choses simples dans le contexte ou si vous vous posez des questions, ce serait:

// 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)
{
}

La première méthode est utilisée lors de l'obtention d'entités spécifiques, la seconde méthode est utilisée spécifiquement lors de l'ajout ou de la modification d'entités sur des pages Web.

Cela nous permet d'avoir le meilleur des deux mondes dans le contexte où ils sont utilisés.


3

Je suis un étudiant en informatique français, alors excusez mon pauvre anglais. Dans nos classes, on nous dit qu'une telle méthode ne devrait jamais retourner null, ni un objet vide. L'utilisateur de cette méthode est censé d'abord vérifier que l'objet qu'il recherche existe avant d'essayer de l'obtenir.

En utilisant Java, on nous demande d'ajouter un assert exists(object) : "You shouldn't try to access an object that doesn't exist";au début de toute méthode qui pourrait retourner null, pour exprimer la "précondition" (je ne sais pas quel est le mot en anglais).

IMO ce n'est vraiment pas facile à utiliser mais c'est ce que j'utilise, en attendant quelque chose de mieux.


1
Merci pour votre réponse. Mais je n'aime pas l'idée de vérifier d'abord si elle existe. La raison en est que génère une requête supplémentaire dans la base de données. Dans une application à laquelle des millions de personnes accèdent en une journée, cela peut entraîner une perte de performances considérable.
7wp

1
Un avantage est que la vérification de l'existence est suffisamment abstraite: si (userExists) est légèrement plus lisible, plus proche du domaine problématique, et moins de `` calcul '' que: if (user == null)
timoxley

Et je dirais que «si (x == null)» est un modèle vieux de plusieurs décennies que si vous ne l'avez pas vu auparavant, vous n'avez pas écrit de code depuis très longtemps (et vous devriez vous y habituer tel qu'il est dans millions de lignes de code). "Informatique"? Nous parlons de l'accès à la base de données ...
Lloyd Sargent

3

Si le cas de l'utilisateur de ne pas être détecté se assez souvent, et que vous voulez faire face à cette de diverses manières en fonction des circonstances (parfois , jeter une exception, en substituant parfois un utilisateur vide) , vous pouvez aussi utiliser quelque chose près de F # de Optionde ou Haskell Maybetype , qui sépare explicitement le cas «sans valeur» de «trouvé quelque chose!». Le code d'accès à la base de données pourrait ressembler à ceci:

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

Et être utilisé comme ceci:

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

Malheureusement, tout le monde semble rouler un type comme celui-ci.


2

Je retourne généralement null. Il fournit un mécanisme rapide et facile pour détecter si quelque chose a foiré sans lancer d'exceptions et en utilisant des tonnes de try / catch partout.


2

Pour les types de collection, je retournerais une collection vide, pour tous les autres types, je préfère utiliser les modèles NullObject pour retourner un objet qui implémente la même interface que celle du type de retour. pour plus de détails sur le motif, consultez le texte du lien

En utilisant le modèle NullObject, ce serait: -

public UserEntity GetUserById(Guid userId)

{// Imaginez du code ici pour accéder à la base de données .....

 //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

Pour dire ce que les autres ont dit de manière plus concise ...

Les exceptions concernent des circonstances exceptionnelles

Si cette méthode est une couche d'accès aux données pure, je dirais qu'étant donné un paramètre qui est inclus dans une instruction select, il s'attendrait à ce que je ne trouve aucune ligne à partir de laquelle construire un objet, et donc retourner null serait acceptable car cela est la logique d'accès aux données.

D'un autre côté, si je m'attendais à ce que mon paramètre reflète une clé primaire et que je ne récupère qu'une ligne, si j'en récupérais plus d'une, je lèverais une exception. 0 est ok pour retourner null, 2 n'est pas.

Maintenant, si j'avais un code de connexion vérifié par rapport à un fournisseur LDAP, puis vérifié par rapport à une base de données pour obtenir plus de détails et que je m'attendais à ce qu'ils soient synchronisés à tout moment, je pourrais alors lancer l'exception. Comme d'autres l'ont dit, ce sont des règles commerciales.

Maintenant, je dirai que c'est une règle générale . Il y a des moments où vous voudrez peut-être briser cela. Cependant, mon expérience et mes expériences avec C # (beaucoup de cela) et Java (un peu de cela) m'ont appris qu'il est beaucoup plus coûteux en termes de performances de traiter les exceptions que de gérer les problèmes prévisibles via une logique conditionnelle. Je parle à hauteur de 2 ou 3 ordres de grandeur plus chers dans certains cas. Donc, s'il est possible que votre code se retrouve dans une boucle, je vous conseillerais de retourner null et de le tester.


2

Pardonnez mon pseudo-php / code.

Je pense que cela dépend vraiment de l'utilisation prévue du résultat.

Si vous souhaitez éditer / modifier la valeur de retour et l'enregistrer, renvoyez un objet vide. De cette façon, vous pouvez utiliser la même fonction pour remplir des données sur un objet nouveau ou existant.

Disons que j'ai une fonction qui prend une clé primaire et un tableau de données, remplit la ligne de données, puis enregistre l'enregistrement résultant dans la base de données. Étant donné que j'ai l'intention de remplir l'objet avec mes données de toute façon, il peut être très avantageux de récupérer un objet vide du getter. De cette façon, je peux effectuer des opérations identiques dans les deux cas. Vous utilisez le résultat de la fonction getter quoi qu'il arrive.

Exemple:

function saveTheRow($prim_key, $data) {
    $row = getRowByPrimKey($prim_key);

    // Populate the data here

    $row->save();
}

Ici, nous pouvons voir que la même série d'opérations manipule tous les enregistrements de ce type.

Cependant, si l'intention ultime de la valeur de retour est de lire et de faire quelque chose avec les données, je retournerais null. De cette façon, je peux déterminer très rapidement si aucune donnée n'a été retournée et afficher le message approprié à l'utilisateur.

Habituellement, j'attraperai des exceptions dans ma fonction qui récupère les données (afin que je puisse enregistrer les messages d'erreur, etc ...) puis retournerai null directement à partir de la capture. Peu importe l'utilisateur final, quel est le problème, je trouve donc préférable d'encapsuler mon enregistrement / traitement des erreurs directement dans la fonction qui obtient les données. Si vous maintenez une base de code partagée dans une grande entreprise, cela est particulièrement avantageux car vous pouvez forcer la journalisation / la gestion des erreurs, même sur le programmeur le plus paresseux.

Exemple:

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

Voilà ma règle générale. Cela a bien fonctionné jusqu'à présent.


2

Question intéressante et je pense qu'il n'y a pas de "bonne" réponse, car cela dépend toujours de la responsabilité de votre code. Votre méthode sait-elle si aucune donnée trouvée ne pose problème ou non? Dans la plupart des cas, la réponse est "non" et c'est pourquoi le retour de la valeur nulle et le fait de laisser l'appelant gérer sa situation est parfait.

Peut-être qu'une bonne approche pour distinguer les méthodes de lancement des méthodes de retour nul est de trouver une convention dans votre équipe: les méthodes qui disent qu'elles "obtiennent" quelque chose devraient lever une exception s'il n'y a rien à obtenir. Les méthodes qui peuvent retourner null peuvent être nommées différemment, peut-être "Find ..." à la place.


+1 J'aime l'idée d'utiliser une convention de dénomination uniforme pour signaler au programmeur comment cette fonction doit être utilisée.
7wp

1
Soudain, je reconnais que c'est ce que fait LINQ: considérer First (...) vs FirstOrDefault (...)
Marc Wittke

2

Si l'objet retourné est quelque chose qui peut être itéré, je retournerais un objet vide, de sorte que je n'ai pas à tester d'abord null.

Exemple:

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

J'aime ne pas retourner null à partir de n'importe quelle méthode, mais utiliser le type fonctionnel Option à la place. Les méthodes qui ne peuvent renvoyer aucun résultat renvoient une option vide, plutôt que null.

En outre, ces méthodes qui ne peuvent renvoyer aucun résultat doivent l'indiquer par leur nom. Je place normalement Try ou TryGet ou TryFind au début du nom de la méthode pour indiquer qu'elle peut retourner un résultat vide (par exemple TryFindCustomer, TryLoadFile, etc.).

Cela permet à l'appelant appliquer différentes techniques, comme la collection pipelining (voir Martin Fowler Pipeline Collection ) sur le résultat.

Voici un autre exemple où le retour d'Option au lieu de null est utilisé pour réduire la complexité du code: Comment réduire la complexité cyclomatique: Type fonctionnel d'option


1
J'ai écrit une réponse, je peux voir qu'elle est similaire à la vôtre en faisant défiler vers le haut, et je suis d'accord, vous pouvez implémenter un type d'option avec une collection générique avec 0 ou 1 éléments. Merci pour les liens supplémentaires.
Gabriel P.

1

Plus de viande à hacher: disons que mon DAL renvoie un NULL pour GetPersonByID comme conseillé par certains. Que doit faire mon BLL (plutôt fin) s'il reçoit un NULL? Transmettez ce NULL et laissez le consommateur final s'en préoccuper (dans ce cas, une page ASP.Net)? Et si le BLL levait une exception?

Le BLL peut être utilisé par ASP.Net et Win App, ou une autre bibliothèque de classes - je pense qu'il est injuste de s'attendre à ce que le consommateur final "sache" intrinsèquement que la méthode GetPersonByID renvoie un null (sauf si des types null sont utilisés, je suppose ).

Ma prise (pour ce que ça vaut) est que mon DAL renvoie NULL si rien n'est trouvé. POUR CERTAINS OBJETS, ce n'est pas grave - cela pourrait être une liste de 0: beaucoup de choses, donc ne rien avoir c'est bien (par exemple une liste de livres préférés). Dans ce cas, mon BLL renvoie une liste vide. Pour la plupart des choses d'entité unique (par exemple, utilisateur, compte, facture) si je n'en ai pas, c'est définitivement un problème et une exception coûteuse. Cependant, étant donné que la récupération d'un utilisateur par un identifiant unique qui a été précédemment donné par l'application doit toujours renvoyer un utilisateur, l'exception est une exception "correcte", comme dans son cas exceptionnel. Le consommateur final du BLL (ASP.Net, f'rinstance) ne s'attend jamais à ce que les choses soient géniales, donc un gestionnaire d'exceptions non géré sera utilisé au lieu d'envelopper chaque appel unique à GetPersonByID dans un bloc try-catch.

S'il y a un problème flagrant dans mon approche, faites-le moi savoir car je suis toujours désireux d'apprendre. Comme d'autres affiches l'ont dit, les exceptions sont des choses coûteuses, et l'approche «vérifier d'abord» est bonne, mais les exceptions devraient être juste cela - exceptionnelles.

J'apprécie ce post, beaucoup de bonnes suggestions pour les scénarios "ça dépend" :-)


Et bien sûr, aujourd'hui, je suis tombé sur un scénario où je vais retourner NULL de mon BLL ;-) Cela dit, je pouvais toujours lever une exception et utiliser try / catch dans ma classe consommatrice MAIS j'ai toujours un problème : comment ma classe consommatrice sait-elle utiliser un try / catch, similaire comment sait-elle pour vérifier NULL?
Mike Kingscott

Vous pouvez documenter qu'une méthode lève une exception via le @throws doctag et vous documenter le fait qu'elle peut retourner null dans le @return doctag.
timoxley

1

Je pense que les fonctions ne devraient pas retourner null, pour la santé de votre base de code. Je peux penser à quelques raisons:

Il y aura une grande quantité de clauses de garde traitant la référence nulle if (f() != null).

Qu'est-ce que c'est null, est-ce une réponse acceptée ou un problème? Null est-il un état valide pour un objet spécifique? (imaginez que vous êtes un client du code). Je veux dire que tous les types de référence peuvent être nuls, mais le devraient-ils?

Le fait de nulltraîner donnera presque toujours quelques exceptions NullRef inattendues de temps en temps à mesure que votre base de code se développera.

Il existe des solutions tester-doer patternou la mise en œuvre de la option typeprogrammation fonctionnelle.


0

Je suis perplexe quant au nombre de réponses (sur le Web) qui disent que vous avez besoin de deux méthodes: une méthode "IsItThere ()" et une méthode "GetItForMe ()", ce qui conduit à une condition de concurrence. Qu'est-ce qui ne va pas avec une fonction qui retourne null, l'affectant à une variable et vérifiant la variable pour Null tout en un test? Mon ancien code C était parsemé de

if (NULL! = (variable = fonction (arguments ...))) {

Vous obtenez donc la valeur (ou null) dans une variable, et le résultat à la fois. Cet idiome a-t-il été oublié? Pourquoi?


0

Je suis d'accord avec la plupart des articles ici, qui tendent vers null.

Mon raisonnement est que la génération d'un objet vide avec des propriétés non nullables peut provoquer des bugs. Par exemple, une entité avec une int IDpropriété aurait une valeur initiale de ID = 0, qui est une valeur entièrement valide. Si cet objet, dans certaines circonstances, devait être enregistré dans la base de données, ce serait une mauvaise chose.

Pour tout ce qui a un itérateur, j'utiliserais toujours la collection vide. Quelque chose comme

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

est une odeur de code à mon avis. Les propriétés de collection ne doivent jamais être nulles.

Un cas de bord est String. Beaucoup de gens disent que ce String.IsNullOrEmptyn'est pas vraiment nécessaire, mais vous ne pouvez pas toujours faire la distinction entre une chaîne vide et null. De plus, certains systèmes de base de données (Oracle) ne les distinguent pas du tout ( ''sont stockés sous DBNULL), vous êtes donc obligé de les gérer également. La raison en est que la plupart des valeurs de chaîne proviennent soit de l'entrée utilisateur ou de systèmes externes, alors que ni les zones de texte ni la plupart des formats d'échange n'ont des représentations différentes pour ''et null. Ainsi, même si l'utilisateur souhaite supprimer une valeur, il ne peut rien faire de plus que d'effacer le contrôle d'entrée. De plus, la distinction des nvarcharchamps de base de données nullable et non nullable est plus que discutable, si votre SGBD n'est pas Oracle - un champ obligatoire qui permet''est bizarre, votre interface utilisateur ne le permettrait jamais, donc vos contraintes ne sont pas mappées. Donc, la réponse ici, à mon avis, est de les gérer également, toujours.

Concernant votre question concernant les exceptions et les performances: si vous lancez une exception que vous ne pouvez pas gérer complètement dans la logique de votre programme, vous devez à un moment donné abandonner ce que fait votre programme et demander à l'utilisateur de refaire ce qu'il vient de faire. Dans ce cas, la pénalité de performance d'un catchest vraiment le moindre de vos soucis - avoir à demander à l'utilisateur est l'éléphant dans la pièce (ce qui signifie re-restituer toute l'interface utilisateur, ou envoyer du HTML via Internet). Donc, si vous ne suivez pas l'anti-modèle de " Flux de programme avec exceptions ", ne vous embêtez pas, jetez-en un si cela a du sens. Même dans les cas limites, tels que "Exception de validation", les performances ne sont vraiment pas un problème, car vous devez de nouveau demander à l'utilisateur, dans tous les cas.


0

Un modèle TryGet asynchrone:

Pour les méthodes synchrones, je crois que la réponse de @Johann Gerell est le modèle à utiliser dans tous les cas.

Cependant, le modèle TryGet avec le outparamètre ne fonctionne pas avec les méthodes Async.

Avec Tuple Literals de C # 7, vous pouvez maintenant faire ceci:

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

        return (true, o);
    }
    else
    {
        return (false, default(SomeObject));
    }
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.