web-dev-qa-db-fra.com

Si les fonctions doivent effectuer des vérifications nulles avant de faire le comportement souhaité, est-ce une mauvaise conception?

Donc, je ne sais pas si c'est une bonne ou une mauvaise conception de code, alors j'ai pensé que je ferais mieux de demander.

Je crée fréquemment des méthodes qui traitent des données impliquant des classes et je vérifie souvent les méthodes pour m'assurer de ne pas obtenir de références nulles ou d'autres erreurs au préalable.

Pour un exemple très basique:

// fields and properties
private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}
public void SetName(string name)
{
    if (_someEntity == null) return; //check to avoid null ref
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

Donc, comme vous pouvez le voir, je vérifie null à chaque fois. Mais la méthode ne devrait-elle pas avoir cette vérification?

Par exemple, le code externe doit nettoyer les données avant la main afin que les méthodes n'aient pas besoin de valider comme ci-dessous:

 if(entity != null) // this makes the null checks redundant in the methods
 {
     Manager.AssignEntity(entity);
     Manager.SetName("Test");
 }

En résumé, les méthodes doivent-elles "valider les données", puis effectuer leur traitement sur les données, ou doivent-elles être garanties avant d'appeler la méthode, et si vous ne validez pas avant d'appeler la méthode, elles devraient générer une erreur (ou intercepter la Erreur)?

66
WDUK

Le problème avec votre exemple de base n'est pas la vérification nulle, c'est l'échec silencieux.

Les erreurs de pointeur/référence nulles sont le plus souvent des erreurs de programmation. Les erreurs de programmation sont souvent mieux gérées en échouant immédiatement et fort. Vous disposez de trois méthodes générales pour résoudre le problème dans cette situation:

  1. Ne vous embêtez pas à vérifier et laissez simplement le runtime lever l'exception nullpointer.
  2. Vérifiez et lancez une exception avec un message mieux que le message NPE de base.

Une troisième solution est plus de travail mais est beaucoup plus robuste:

  1. Structurez votre classe de telle sorte qu'il soit pratiquement impossible pour _someEntity pour ne jamais être dans un état non valide. Une façon de le faire est de se débarrasser de AssignEntity et de l'exiger comme paramètre pour l'instanciation. D'autres techniques d'injection de dépendance peuvent également être utiles.

Dans certaines situations, il est logique de vérifier la validité de tous les arguments de la fonction avant le travail que vous faites, et dans d'autres, il est logique de dire à l'appelant qu'il est responsable de s'assurer que ses entrées sont valides et non de vérifier. Quelle extrémité du spectre vous dépendez dépendra de votre domaine de problème. La troisième option présente un avantage significatif dans la mesure où vous pouvez, dans une certaine mesure, demander au compilateur de faire en sorte que l'appelant fasse tout correctement.

Si la troisième option n'est pas une option, à mon avis, cela n'a vraiment pas d'importance tant qu'elle n'échoue pas silencieusement. Si le fait de ne pas vérifier la valeur nulle fait exploser instantanément le programme, c'est bien, mais s'il corrompt à la place les données pour causer des problèmes en cours de route, il est préférable de les vérifier et de les traiter tout de suite.

168
whatsisname

Depuis _someEntity peut être modifié à tout moment, il est donc logique de le tester chaque fois que SetName est appelé. Après tout, cela aurait pu changer depuis le dernier appel de cette méthode. Mais sachez que le code dans SetName n'est pas adapté aux threads, vous pouvez donc effectuer cette vérification dans un thread, avoir _someEntity défini sur null par un autre, puis le code réduira un NullReferenceException de toute façon.

Une approche de ce problème consiste donc à devenir encore plus défensif et à effectuer l'une des actions suivantes:

  1. faire une copie locale de _someEntity et référencez cette copie via la méthode,
  2. utilisez un verrou autour de _someEntity pour s'assurer qu'elle ne change pas pendant l'exécution de la méthode,
  3. utiliser un try/catch et effectuez la même action sur tous les NullReferenceException qui se produisent dans la méthode que vous effectueriez lors de la vérification Null initiale (dans ce cas, une action nop).

Mais ce que vous devez vraiment faire, c'est vous arrêter et vous poser la question: devez-vous réellement autoriser _someEntity à écraser? Pourquoi ne pas le définir une fois, via le constructeur. De cette façon, vous n'avez besoin que de faire cette vérification nulle une fois:

private readonly Entity _someEntity;

public Constructor(Entity someEntity)
{
    if (someEntity == null) throw new ArgumentNullException(nameof(someEntity));
    _someEntity = someEntity;
}

public void SetName(string name)
{
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

Si possible, c'est l'approche que je recommanderais d'adopter dans de telles situations. Si vous ne pouvez pas faire attention à la sécurité des threads lorsque vous choisissez comment gérer les null possibles.

En bonus, je pense que votre code est étrange et pourrait être amélioré. Tous:

private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}

peut être remplacé par:

public Entity SomeEntity { get; set; }

Qui a la même fonctionnalité, sauf pour pouvoir définir le champ via le setter de propriétés, plutôt qu'une méthode avec un nom différent.

55
David Arno

Mais la méthode ne devrait-elle pas avoir cette vérification?

C'est votre choix.

En créant une méthode public, vous offrez à public la possibilité de l'appeler. Cela s'accompagne toujours d'un contrat implicite sur comment pour appeler cette méthode et à quoi s'attendre en le faisant. Ce contrat peut (ou peut ne pas) inclure "ouais, uhhm, si vous passez null comme valeur de paramètre, il explosera en plein visage.". D'autres parties de ce contrat pourraient être "oh, BTW: chaque fois que deux threads exécutent cette méthode en même temps, un chaton meurt".

Le type de contrat que vous souhaitez concevoir vous appartient. Les choses importantes sont de

  • créer une API utile et cohérente et

  • pour bien le documenter, en particulier pour les cas Edge.

Quels sont les cas probables comment votre méthode est appelée?

23
null

Les autres réponses sont bonnes; Je voudrais les étendre en faisant quelques commentaires sur les modificateurs d'accessibilité. Tout d'abord, que faire si null n'est jamais valide:

  • Les méthodes public et protected sont celles où vous ne contrôlez pas l'appelant. Ils devraient jeter lorsqu'ils sont passés null. De cette façon, vous entraînez vos appelants à ne jamais vous laisser passer un zéro, car ils aimeraient que leur programme ne plante pas.

  • interne et privé les méthodes sont celles où vous contrôlez l'appelant. Ils doivent affirmer lorsqu'ils sont passés null; ils se bloqueront alors probablement plus tard, mais au moins vous avez obtenu l'assertion en premier, et l'occasion de pénétrer dans le débogueur. Encore une fois, vous voulez former vos appelants à vous appeler correctement en leur faisant mal quand ils ne le font pas.

Maintenant, que faites-vous s'il peut être valide pour qu'un null soit transmis? J'éviterais cette situation avec le modèle d'objet nul . C'est-à-dire: créer une instance spéciale du type et exiger qu'elle soit utilisée chaque fois qu'un objet invalide est nécessaire . Maintenant, vous êtes de retour dans le monde agréable de lancer/affirmer à chaque utilisation de null.

Par exemple, vous ne voulez pas être dans cette situation:

class Person { ... }
...
class ExpenseReport { 
  public void SubmitReport(Person approver) { 
    if (approver == null) ... do something ...

et sur le site de l'appel:

// I guess this works but I don't know what it means
expenses.SubmitReport(null); 

Parce que (1) vous apprenez à vos appelants que null est une bonne valeur, et (2) il n'est pas clair quelle est la sémantique de cette valeur. Faites plutôt ceci:

class ExpenseReport { 
  public static readonly Person ApproverNotRequired = new Person();
  public static readonly Person ApproverUnknown = new Person();
  ...
  public void SubmitReport(Person approver) { 
    if (approver == null) throw ...
    if (approver == ApproverNotRequired) ... do something ...
    if (approver == ApproverUnknown) ... do something ...

Et maintenant, le site d'appel ressemble à ceci:

expenses.SubmitReport(ExpenseReport.ApproverNotRequired);

et maintenant le lecteur du code sait ce que cela signifie.

14
Eric Lippert

Les autres réponses soulignent que votre code peut être nettoyé pour ne pas avoir besoin d'une vérification nulle où vous l'avez, cependant, pour une réponse générale sur ce qu'une vérification nulle peut être utile, considérez l'exemple de code suivant:

public static class Foo {
    public static void Frob(Bar a, Bar b) {
        if (a == null) throw new ArgumentNullException(nameof(a));
        if (b == null) throw new ArgumentNullException(nameof(b));

        a.Something = b.Something;
    }
}

Cela vous donne un avantage pour la maintenance. S'il se produit un défaut dans votre code où quelque chose transmet un objet nul à la méthode, vous obtiendrez des informations utiles de la méthode quant au paramètre qui était nul. Sans les contrôles, vous obtiendrez une NullReferenceException sur la ligne:

a.Something = b.Something;

Et (étant donné que la propriété Something est un type de valeur), a ou b peuvent être nuls et vous n'aurez aucun moyen de savoir lequel à partir de la trace de la pile. Avec les vérifications, vous saurez exactement quel objet était nul, ce qui peut être extrêmement utile lorsque vous essayez de déceler un défaut.

Je voudrais noter que le modèle de votre code d'origine:

if (x == null) return;

Peut avoir ses utilisations dans certaines circonstances, il peut également (peut-être plus souvent) vous donner un très bon moyen de masquer les problèmes sous-jacents dans votre base de code.

8
Paddy

Non pas du tout, mais ça dépend.

Vous ne faites une vérification de référence nulle que si vous voulez y réagir.

Si vous faites une vérification de référence nulle et lancez une exception vérifiée, vous forcez l'utilisateur de la méthode à réagir dessus et lui permettez de récupérer. Le programme fonctionne toujours comme prévu.

Si vous ne faites pas de vérification de référence nulle (ou lancez une exception non vérifiée), cela pourrait lancer un NullReferenceException non contrôlé, qui n'est généralement pas géré par l'utilisateur de la méthode et même pourrait fermer l'application. Cela signifie que la fonction est évidemment complètement incomprise et donc l'application est défectueuse.

4
Herr Derb

Quelque chose comme une extension de la réponse de @ null, mais d'après mon expérience, faites des vérifications null peu importe quoi.

Une bonne programmation défensive est l'attitude selon laquelle vous codez la routine actuelle de manière isolée, en supposant que le reste du programme essaie de la bloquer. Lorsque vous écrivez du code essentiel à une mission où l'échec n'est pas une option, c'est à peu près la seule façon de procéder.

Maintenant, comment vous traitez réellement une valeur null, cela dépend beaucoup de votre cas d'utilisation.

Si null est une valeur acceptable, par exemple l'un des deuxième à cinquième paramètres de la routine MSVC _splitpath (), puis le traiter en silence est le comportement correct.

Si null ne doit jamais se produire lors de l'appel de la routine en question, c'est-à-dire que dans le cas de l'OP, le contrat API requiert que AssignEntity() ait été appelé avec un Entity valide, puis lancez un exception, ou sinon échouer en vous assurant de faire beaucoup de bruit dans le processus.

Ne vous inquiétez jamais du coût d'un contrôle nul, ils sont très bon marché s'ils se produisent, et avec les compilateurs modernes, l'analyse de code statique peut les éliminer complètement si le compilateur détermine que cela ne se produira pas.

4
dgnuff