Meilleures pratiques: lever des exceptions à partir de propriétés

111

Quand est-il approprié de lever une exception depuis un getter ou un setter de propriété? Quand n'est-ce pas approprié? Pourquoi? Des liens vers des documents externes sur le sujet seraient utiles ... Google est étonnamment peu apparu.

Jon Seigel
la source
Aussi lié: stackoverflow.com/questions/1488322/…
Brian Rasmussen
1
J'ai lu ces deux questions, mais aucune n'a répondu complètement à cette question IMO.
Jon Seigel
Chaque fois que nécessaire. Les questions et les réponses précédentes montrent qu'il est permis et apprécié de lever des exceptions à partir du getter ou du setter, de sorte que vous pouvez simplement "être intelligent".
Lex Li

Réponses:

135

Microsoft a ses recommandations sur la façon de concevoir des propriétés à l' adresse http://msdn.microsoft.com/en-us/library/ms229006.aspx

Essentiellement, ils recommandent que les getters de propriété soient des accesseurs légers qui peuvent toujours être appelés en toute sécurité. Ils recommandent de redessiner les getters pour qu'ils deviennent des méthodes si des exceptions sont quelque chose que vous devez lancer. Pour les setters, ils indiquent que les exceptions sont une stratégie de gestion des erreurs appropriée et acceptable.

Pour les indexeurs, Microsoft indique qu'il est acceptable pour les getters et les setters de lever des exceptions. Et en fait, de nombreux indexeurs de la bibliothèque .NET le font. L'exception la plus courante étant ArgumentOutOfRangeException.

Il y a de très bonnes raisons pour lesquelles vous ne voulez pas lever d'exceptions dans les getters de propriété:

  • Étant donné que les propriétés «semblent» être des champs, il n'est pas toujours évident qu'elles peuvent lever une exception (par conception); tandis qu'avec les méthodes, les programmeurs sont formés pour s'attendre et rechercher si les exceptions sont une conséquence attendue de l'appel de la méthode.
  • Les Getters sont utilisés par de nombreuses infrastructures .NET, comme les sérialiseurs et la liaison de données (dans WinForms et WPF par exemple) - le traitement des exceptions dans de tels contextes peut rapidement devenir problématique.
  • Les récupérateurs de propriétés sont automatiquement évalués par les débogueurs lorsque vous regardez ou inspectez un objet. Une exception ici peut être déroutante et ralentir vos efforts de débogage. Il n'est également pas souhaitable d'effectuer d'autres opérations coûteuses dans les propriétés (comme l'accès à une base de données) pour les mêmes raisons.
  • Les propriétés sont souvent utilisées dans une convention de chaînage: obj.PropA.AnotherProp.YetAnother- avec ce type de syntaxe, il devient problématique de décider où injecter les instructions exception catch.

En remarque, il faut savoir que ce n'est pas parce qu'une propriété n'est pas conçue pour lever une exception que cela ne signifie pas qu'elle ne le sera pas; il pourrait facilement appeler le code qui le fait. Même le simple fait d'allouer un nouvel objet (comme une chaîne) peut entraîner des exceptions. Vous devez toujours écrire votre code de manière défensive et vous attendre à des exceptions de tout ce que vous invoquez.

LBushkin
la source
41
Si vous rencontrez une exception fatale telle que "mémoire insuffisante", peu importe que vous obteniez l'exception dans une propriété ou ailleurs. Si vous ne l'avez pas dans la propriété, vous l'obtiendrez juste quelques nanosecondes plus tard à partir de la prochaine chose qui alloue de la mémoire. La question n'est pas "une propriété peut-elle lever une exception?" Presque tout le code peut lever une exception en raison d'une condition fatale. La question est de savoir si une propriété devrait, par conception, lever une exception dans le cadre de son contrat spécifique.
Eric Lippert
1
Je ne suis pas sûr de comprendre l'argument de cette réponse. Par exemple, en ce qui concerne la liaison de données - WinForms et WPF sont spécifiquement écrits pour gérer correctement les exceptions levées par les propriétés et pour les traiter comme des échecs de validation - ce qui est un moyen parfaitement correct (certains pensent même que c'est le meilleur) de fournir une validation de modèle de domaine .
Pavel Minaev
6
@Pavel - alors que WinForms et WPF peuvent récupérer gracieusement des exceptions dans les accesseurs de propriété, il n'est pas toujours facile d'identifier et de récupérer de telles erreurs. Dans certains cas (par exemple, lorsque dans WPF un setter de modèle de contrôle lève une exception), l'exception est avalée en silence. Cela peut conduire à des sessions de débogage douloureuses si vous n'avez jamais rencontré de tels cas auparavant.
LBushkin
1
@Steven: Alors, à quel point ce cours vous a-t-il servi dans le cas exceptionnel? Si vous avez ensuite dû écrire du code défensif pour gérer toutes ces exceptions en raison d'un échec, et vraisemblablement fournir des valeurs par défaut appropriées, pourquoi ne pas fournir ces valeurs par défaut dans votre capture? Sinon, si les exceptions de propriété sont levées à l'utilisateur, pourquoi ne pas simplement lancer l'exception "InvalidArgumentException" d'origine ou similaire afin qu'ils puissent fournir le fichier de paramètres manquant?
Zhaph - Ben Duguid
6
Il y a une raison pour laquelle ce sont des lignes directrices et non des règles; aucune directive ne couvre tous les cas de bord fou. J'aurais probablement fait ces méthodes et non des propriétés moi-même, mais c'est une question de jugement.
Eric Lippert
34

Il n'y a rien de mal à lancer des exceptions de la part des setters. Après tout, quelle meilleure façon d'indiquer que la valeur n'est pas valide pour une propriété donnée?

Pour les getters, il est généralement mal vu, et cela peut être expliqué assez facilement: un getter de propriété, en général, rapporte l'état actuel d'un objet; ainsi, le seul cas où il est raisonnable pour un getter de lancer est celui où l'état est invalide. Mais il est également généralement considéré comme une bonne idée de concevoir vos classes de telle sorte qu'il ne soit tout simplement pas possible d'obtenir un objet invalide au départ, ou de le mettre dans un état invalide par des moyens normaux (c'est-à-dire toujours assurer une initialisation complète dans les constructeurs, et essayez de rendre les méthodes sans exception en ce qui concerne la validité d'état et les invariants de classe). Tant que vous vous en tenez à cette règle, vos getters de propriété ne devraient jamais se retrouver dans une situation où ils doivent signaler un état invalide, et donc ne jamais lancer.

Il y a une exception que je connais, et c'est en fait une exception assez importante: toute implémentation d'objet IDisposable. Disposeest spécifiquement conçu comme un moyen de mettre un objet dans un état invalide, et il existe même une classe d'exception spéciale ObjectDisposedException, à utiliser dans ce cas. Il est parfaitement normal de lancer ObjectDisposedExceptiondepuis n'importe quel membre de classe, y compris les récupérateurs de propriété (et s'excluant Disposelui-même), après que l'objet a été supprimé.

Pavel Minaev
la source
4
Merci Pavel. Cette réponse va dans le «pourquoi» au lieu de simplement déclarer à nouveau que ce n'est pas une bonne idée de lever une exception des propriétés.
SolutionYogi
1
Je n'aime pas l'idée que tous les membres d'un an IDisposabledevraient être rendus inutiles après un Dispose. Si l'invocation d'un membre nécessiterait l'utilisation d'une ressource qui Disposea rendu indisponible (par exemple, le membre lirait les données d'un flux qui a été fermé) le membre devrait lancer ObjectDisposedExceptionplutôt que de fuir par exemple ArgumentException, mais si l'on a un formulaire avec des propriétés qui représentent le dans certains champs, il semblerait beaucoup plus utile de permettre la lecture de ces propriétés après l'élimination (donnant les dernières valeurs typées) que d'exiger ...
supercat
1
... qui Disposeseront différés jusqu'à ce que toutes ces propriétés aient été lues. Dans certains cas où un thread peut utiliser des lectures bloquantes sur un objet tandis qu'un autre le ferme, et où des données peuvent arriver à tout moment avant Dispose, il peut être utile d'avoir Disposecoupé les données entrantes, mais autoriser la lecture des données précédemment reçues. Il ne faut pas forcer une distinction artificielle entre Closeet Disposedans des situations où aucune n'aurait autrement besoin d'exister.
supercat
Comprendre la raison de la règle permet de savoir quand enfreindre la règle (Raymond Chen). Dans ce cas, nous pouvons voir que s'il y a une erreur irrécupérable de n'importe quel type, vous ne devriez pas la cacher dans le getter, car dans de tels cas, l'application doit quitter dès que possible.
Ben
Le point que j'essayais de faire valoir est que vos getters de propriété ne devraient généralement pas contenir de logique permettant des erreurs irrécupérables. Si c'est le cas, il se peut que ce soit mieux comme Get...méthode à la place. Une exception ici est lorsque vous devez implémenter une interface existante qui vous oblige à fournir une propriété.
Pavel Minaev
24

Il n'est presque jamais approprié sur un getter, et parfois approprié sur un setter.

La meilleure ressource pour ce genre de questions est "Framework Design Guidelines" par Cwalina et Abrams; il est disponible sous forme de livre relié, et une grande partie de celui-ci est également disponible en ligne.

De la section 5.2: Conception de la propriété

ÉVITEZ de lancer des exceptions à partir des récupérateurs de propriété. Les récupérateurs de propriétés doivent être des opérations simples et ne doivent pas avoir de conditions préalables. Si un getter peut lever une exception, il devrait probablement être repensé pour être une méthode. Notez que cette règle ne s'applique pas aux indexeurs, pour lesquels nous attendons des exceptions suite à la validation des arguments.

Notez que cette directive s'applique uniquement aux récupérateurs de propriétés. Il est possible de lever une exception dans un setter de propriétés.

Eric Lippert
la source
2
Bien que (en général) je suis d'accord avec ces directives, il pense qu'il est utile de fournir un aperçu supplémentaire des raisons pour lesquelles elles devraient être suivies - et du type de conséquences qui peuvent survenir lorsqu'elles sont ignorées.
LBushkin
3
Comment cela se rapporte-t-il aux objets jetables et aux conseils que vous devriez envisager de lancer ObjectDisposedExceptionune fois que l'objet a été Dispose()appelé et que quelque chose demande par la suite une valeur de propriété? Il semble que les conseils devraient être "éviter de lancer des exceptions à partir des récupérateurs de propriété, à moins que l'objet n'ait été supprimé, auquel cas vous devriez envisager de lancer un ObjectDisposedExcpetion".
Scott Dorman
4
Le design est l'art et la science de trouver des compromis raisonnables face à des exigences contradictoires. Quoi qu'il en soit, cela semble être un compromis raisonnable; Je ne serais pas surpris d'avoir un objet disposé sur une propriété; je ne serais pas non plus surpris si ce n'était pas le cas. Étant donné que l'utilisation d'un objet supprimé est une pratique de programmation terrible, il serait imprudent d'avoir des attentes.
Eric Lippert
1
Un autre scénario dans lequel il est tout à fait valide de lever des exceptions depuis les getters est lorsqu'un objet utilise des invariants de classe pour valider son état interne, qui doivent être vérifiés chaque fois qu'un accès public est effectué, qu'il s'agisse d'une méthode ou d'une propriété
Trap
2

Une bonne approche des exceptions consiste à les utiliser pour documenter le code pour vous-même et pour d'autres développeurs comme suit:

Les exceptions devraient concerner les états de programme exceptionnels. Cela signifie que vous pouvez les écrire où vous voulez!

Une des raisons pour lesquelles vous voudrez peut-être les mettre dans des getters est de documenter l'API d'une classe - si le logiciel lève une exception dès qu'un programmeur essaie de l'utiliser de manière incorrecte, il ne l'utilisera pas mal! Par exemple, si vous avez une validation pendant un processus de lecture de données, il peut ne pas être logique de pouvoir continuer et d'accéder aux résultats du processus s'il y avait des erreurs fatales dans les données. Dans ce cas, vous souhaiterez peut-être obtenir la sortie de sortie s'il y avait des erreurs pour vous assurer qu'un autre programmeur vérifie cette condition.

Ils sont une manière de documenter les hypothèses et les limites d'un sous-système / méthode / peu importe. Dans le cas général, ils ne doivent pas être pris! C'est aussi parce qu'ils ne sont jamais lancés si le système fonctionne ensemble de la manière attendue: si une exception se produit, cela montre que les hypothèses d'un morceau de code ne sont pas satisfaites - par exemple, il n'interagit pas avec le monde qui l'entoure de la manière c'était initialement prévu. Si vous détectez une exception qui a été écrite à cet effet, cela signifie probablement que le système est entré dans un état imprévisible / incohérent - cela peut finalement conduire à un plantage ou à une corruption de données ou similaire, ce qui sera probablement beaucoup plus difficile à détecter / déboguer.

Les messages d'exception sont un moyen très grossier de signaler les erreurs - ils ne peuvent pas être collectés en masse et ne contiennent vraiment qu'une chaîne. Cela les rend impropres à signaler des problèmes dans les données d'entrée. En fonctionnement normal, le système lui-même ne doit pas entrer dans un état d'erreur. En conséquence, les messages qu'ils contiennent doivent être conçus pour les programmeurs et non pour les utilisateurs - les choses qui ne sont pas correctes dans les données d'entrée peuvent être découvertes et relayées aux utilisateurs dans des formats (personnalisés) plus appropriés.

L'exception (haha!) À cette règle est des choses comme IO où les exceptions ne sont pas sous votre contrôle et ne peuvent pas être vérifiées à l'avance.

JonnyRaa
la source
2
Comment cette réponse valide et pertinente a-t-elle été rejetée? Il ne devrait y avoir aucune politique dans StackOverflow, et si cette réponse semble manquer la cible, ajoutez un commentaire à cet effet. Le vote à la baisse concerne les réponses non pertinentes ou erronées.
débatteur
1

Tout cela est documenté dans MSDN (comme lié à d'autres réponses), mais voici une règle générale ...

Dans le setter, si votre propriété doit être validée au-dessus et au-delà du type. Par exemple, une propriété appelée PhoneNumber devrait probablement avoir une validation regex et devrait générer une erreur si le format n'est pas valide.

Pour les getters, peut-être lorsque la valeur est nulle, mais c'est probablement quelque chose que vous voudrez gérer sur le code appelant (selon les directives de conception).

David Stratton
la source
0

C'est une question très complexe et la réponse dépend de la façon dont votre objet est utilisé. En règle générale, les getters et les setters de propriété qui sont "à liaison tardive" ne devraient pas lancer d'exceptions, tandis que les propriétés avec exclusivement une "liaison anticipée" devraient lever des exceptions lorsque le besoin s'en fait sentir. BTW, l'outil d'analyse de code de Microsoft définit l'utilisation des propriétés de manière trop étroite à mon avis.

«liaison tardive» signifie que les propriétés sont trouvées par réflexion. Par exemple, l'attribut Serializeable "est utilisé pour sérialiser / désérialiser un objet via ses propriétés. Lancer une exception dans ce genre de situation casse les choses de manière catastrophique et n'est pas un bon moyen d'utiliser des exceptions pour créer un code plus robuste.

«Early binding» signifie qu'une propriété utilisée est liée dans le code par le compilateur. Par exemple, lorsque du code que vous écrivez fait référence à un getter de propriété. Dans ce cas, il est possible de lancer des exceptions lorsqu'elles ont un sens.

Un objet avec des attributs internes a un état déterminé par les valeurs de ces attributs. Les propriétés exprimant des attributs qui sont conscients et sensibles à l'état interne de l'objet ne doivent pas être utilisées pour la liaison tardive. Par exemple, disons que vous avez un objet qui doit être ouvert, accédé, puis fermé. Dans ce cas, accéder aux propriétés sans appeler open en premier devrait entraîner une exception. Supposons, dans ce cas, que nous ne lançions pas d'exception et que nous permettions au code d'accéder à une valeur sans lever d'exception? Le code semblera heureux même s'il a obtenu une valeur d'un getter qui n'a pas de sens. Maintenant, nous avons mis le code qui a appelé le getter dans une mauvaise situation car il doit savoir comment vérifier la valeur pour voir si elle n'a pas de sens. Cela signifie que le code doit faire des hypothèses sur la valeur qu'il a obtenue du getter de propriété afin de le valider. C'est ainsi que le mauvais code est écrit.

Jack D Menendez
la source
0

J'avais ce code où je ne savais pas quelle exception lancer.

public Person
{
    public string Name { get; set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    if (person.Name == null) {
        throw new Exception("Name of person is null.");
        // I was unsure of which exception to throw here.
    }

    Console.WriteLine("Name is: " + person.Name);
}

J'ai empêché le modèle d'avoir la propriété null en premier lieu en le forçant comme argument dans le constructeur.

public Person
{
    public Person(string name)
    {
        if (name == null) {
            throw new ArgumentNullException(nameof(name));
        }
        Name = name;
    }

    public string Name { get; private set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    Console.WriteLine("Name is: " + person.Name);
}
Fred
la source