Debug.Assert vs lancement d'exceptions

93

J'ai lu de nombreux articles (et quelques autres questions similaires qui ont été publiées sur StackOverflow) sur la manière et le moment d'utiliser les assertions, et je les ai bien comprises. Mais encore, je ne comprends pas quel genre de motivation devrait me pousser à utiliser Debug.Assertau lieu de lancer une simple exception. Ce que je veux dire, c'est que dans .NET, la réponse par défaut à une assertion échouée est «d'arrêter le monde» et d'afficher une boîte de message à l'utilisateur. Bien que ce type de comportement puisse être modifié, je trouve cela très ennuyeux et redondant de le faire, alors que je pourrais plutôt lancer une exception appropriée. De cette façon, je pourrais facilement écrire l'erreur dans le journal de l'application juste avant de lancer l'exception, et de plus, mon application ne se fige pas nécessairement.

Alors, pourquoi devrais-je, le cas échéant, utiliser Debug.Assertau lieu d'une simple exception? Placer une assertion là où elle ne devrait pas être pourrait simplement provoquer toutes sortes de "comportements indésirables", donc à mon avis, je ne gagne vraiment rien en utilisant une assertion au lieu de lancer une exception. Êtes-vous d'accord avec moi ou est-ce que je manque quelque chose ici?

Remarque: Je comprends parfaitement quelle est la différence "en théorie" (Debug vs Release, modèles d'utilisation, etc.), mais comme je le vois, je ferais mieux de lancer une exception au lieu d'effectuer une assertion. Puisque si un bogue est découvert sur une version de production, je voudrais toujours que "l'assertion" échoue (après tout, le "surcoût" est ridiculement petit), donc je ferai mieux de lancer une exception à la place.


Edit: La façon dont je le vois, si une assertion échoue, cela signifie que l'application est entrée dans une sorte d'état corrompu et inattendu. Alors pourquoi voudrais-je continuer l'exécution? Peu importe si l'application s'exécute sur une version de débogage ou de publication. La même chose vaut pour les deux

Communauté
la source
1
Pour les choses que vous dites "si un bogue est découvert sur une version de production, je voudrais toujours que" l'assertion "échoue", les exceptions sont ce que vous devriez utiliser
Tom Neyland
1
La performance est la seule raison. Null vérifier tout tout le temps peut réduire la vitesse, même si cela peut être complètement invisible. C'est principalement pour les cas qui ne devraient jamais arriver, par exemple, vous savez que vous l'avez déjà vérifié par null dans une fonction précédente, il est inutile de perdre des cycles à le vérifier à nouveau. Le debug.assert agit efficacement comme un test unitaire de dernière chance pour vous informer.
lance le

Réponses:

176

Bien que je convienne que votre raisonnement est plausible - c'est-à-dire que si une affirmation est violée de manière inattendue, il est logique d'arrêter l'exécution en lançant - je n'utiliserais personnellement pas d'exceptions à la place des affirmations. Voici pourquoi:

Comme d'autres l'ont dit, les affirmations devraient documenter des situations qui sont impossibles , de telle manière que si la situation prétendument impossible se produit, le développeur en est informé. Les exceptions, en revanche, fournissent un mécanisme de contrôle du flux pour les situations exceptionnelles, improbables ou erronées, mais pas les situations impossibles. Pour moi, la principale différence est la suivante:

  • Il devrait TOUJOURS être possible de produire un cas de test qui exerce une instruction throw donnée. S'il n'est pas possible de produire un tel cas de test, alors vous avez un chemin de code dans votre programme qui ne s'exécute jamais, et il doit être supprimé en tant que code mort.

  • Il ne devrait JAMAIS être possible de produire un cas de test qui déclenche une assertion. Si une assertion se déclenche, soit le code est erroné, soit l'assertion est fausse; de toute façon, quelque chose doit changer dans le code.

C'est pourquoi je ne remplacerais pas une assertion par une exception. Si l'assertion ne peut pas réellement se déclencher, la remplacer par une exception signifie que vous avez un chemin de code non testable dans votre programme . Je n'aime pas les chemins de code non testables.

Eric Lippert
la source
16
Le problème avec les assertions est qu'elles ne sont pas présentes dans la version de production. L'échec d'une condition supposée signifie que votre programme est entré dans un terrain de comportement indéfini, auquel cas un programme responsable doit interrompre son exécution dès que possible (le déroulement de la pile est également quelque peu dangereux, selon la rigueur que vous souhaitez obtenir). Oui, les affirmations devraient généralement être impossibles à tirer, mais vous ne savez pas ce qui est possible lorsque les choses se passent dans la nature. Ce que vous pensiez impossible pourrait se produire en production, et un programme responsable devrait détecter les hypothèses violées et agir rapidement.
kizzx2
2
@ kizzx2: OK, alors combien d'exceptions impossibles par ligne de code de production écrivez-vous?
Eric Lippert
4
@ kixxx2: Ceci est C #, vous pouvez donc conserver les assertions dans le code de production en utilisant Trace.Assert. Vous pouvez même utiliser le fichier app.config pour rediriger les assertions de production vers un fichier texte plutôt que d'être impoli envers l'utilisateur final.
HTTP 410
3
@AnorZaken: Votre point illustre un défaut de conception dans les exceptions. Comme je l'ai noté ailleurs, les exceptions sont (1) des catastrophes mortelles, (2) des erreurs stupides qui ne devraient jamais se produire, (3) des échecs de conception où une exception est utilisée pour signaler une condition non exceptionnelle, ou (4) des conditions exogènes inattendues . Pourquoi ces quatre choses complètement différentes sont-elles toutes représentées par des exceptions? Si j'avais le choix, crétin « null a été déréférencé » exceptions ne seraient pas saisissable du tout . Ce n'est jamais correct et cela devrait mettre fin à votre programme avant qu'il ne fasse plus de mal . Ils devraient ressembler davantage à des affirmations.
Eric Lippert
2
@Backwards_Dave: les fausses assertions sont mauvaises, pas vraies. Les assertions vous permettent d'exécuter des contrôles de vérification coûteux que vous ne souhaiteriez pas exécuter en production. Et si une assertion est violée en production, que doit faire l'utilisateur final à ce sujet?
Eric Lippert
17

Les assertions sont utilisées pour vérifier la compréhension du monde par le programmeur. Une assertion ne doit échouer que si le programmeur a fait quelque chose de mal. Par exemple, n'utilisez jamais une assertion pour vérifier l'entrée utilisateur.

Teste les conditions qui "ne peuvent pas arriver". Les exceptions concernent les conditions qui "ne devraient pas se produire mais se produisent".

Les assertions sont utiles car au moment de la construction (ou même au moment de l'exécution), vous pouvez modifier leur comportement. Par exemple, souvent dans les versions de version, les assertions ne sont même pas vérifiées, car elles introduisent une surcharge inutile. Il faut également se méfier de cela: vos tests peuvent même ne pas être exécutés.

Si vous utilisez des exceptions au lieu d'assertions, vous perdez de la valeur:

  1. Le code est plus détaillé, car tester et lancer une exception fait au moins deux lignes, tandis qu'une assert n'en est qu'une.

  2. Votre code de test et de lancement sera toujours exécuté, tandis que les assertions peuvent être compilées.

  3. Vous perdez une certaine communication avec les autres développeurs, car les assertions ont une signification différente de celle du code produit qui vérifie et lance. Si vous testez vraiment une assertion de programmation, utilisez une assert.

Plus ici: http://nedbatchelder.com/text/assert.html

Ned Batchelder
la source
Si cela "ne peut pas arriver", alors pourquoi écrire une assertion. N'est-ce pas redondant? Si cela peut réellement arriver mais ne devrait pas, alors n'est-ce pas la même chose que "ne devrait pas arriver mais faire" qui est pour les exceptions?
David Klempfner
2
"Ne peut pas arriver" est entre guillemets pour une raison: cela ne peut arriver que si le programmeur a fait quelque chose de mal dans une autre partie du programme. L'assertion est un contrôle contre les erreurs du programmeur.
Ned Batchelder
@NedBatchelder Le terme programmeur est cependant un peu ambigu lorsque vous développez une bibliothèque. Est-il juste que ces «situations impossibles» devraient être impossibles par l'utilisateur de la bibliothèque, mais pourraient être possibles lorsque l'auteur de la bibliothèque a fait une erreur?
Bruno Zell
12

EDIT: En réponse à la modification / note que vous avez faite dans votre message: Il semble que l'utilisation d'exceptions soit la bonne chose à utiliser plutôt que d'utiliser des assertions pour le type de choses que vous essayez d'accomplir. Je pense que la pierre d'achoppement mentale que vous rencontrez est que vous envisagez des exceptions et des affirmations pour atteindre le même objectif, et vous essayez donc de déterminer laquelle serait «bonne» à utiliser. Bien qu'il puisse y avoir un certain chevauchement dans la façon dont les assertions et les exceptions peuvent être utilisées, ne confondez pas cela car elles sont des solutions différentes au même problème - elles ne le sont pas. Les affirmations et les exceptions ont chacune leur but, leurs forces et leurs faiblesses.

J'allais taper une réponse dans mes propres mots mais cela rend le concept plus juste que je ne l'aurais fait:

Station C #: Assertions

L'utilisation d'instructions assert peut être un moyen efficace de détecter les erreurs de logique du programme au moment de l'exécution, et pourtant elles sont facilement filtrées hors du code de production. Une fois le développement terminé, le coût d'exécution de ces tests redondants pour les erreurs de codage peut être éliminé simplement en définissant le symbole du préprocesseur NDEBUG [qui désactive toutes les assertions] lors de la compilation. Cependant, n'oubliez pas que le code placé dans l'assert lui-même sera omis dans la version de production.

Une assertion est mieux utilisée pour tester une condition uniquement lorsque tous les éléments suivants sont maintenus:

* the condition should never be false if the code is correct,
* the condition is not so trivial so as to obviously be always true, and
* the condition is in some sense internal to a body of software.

Les assertions ne devraient presque jamais être utilisées pour détecter des situations qui surviennent pendant le fonctionnement normal du logiciel. Par exemple, les assertions ne doivent généralement pas être utilisées pour rechercher des erreurs dans l'entrée d'un utilisateur. Il peut cependant être judicieux d'utiliser des assertions pour vérifier qu'un appelant a déjà vérifié l'entrée d'un utilisateur.

Fondamentalement, utilisez des exceptions pour les choses qui doivent être interceptées / traitées dans une application de production, utilisez des assertions pour effectuer des vérifications logiques qui seront utiles pour le développement mais désactivées en production.

Tom Neyland
la source
Je réalise tout cela. Mais le fait est que la même déclaration que vous avez marquée en gras s'applique également aux exceptions. Donc la façon dont je le vois, au lieu d'une assertion, je pourrais simplement lever une exception (car si la "situation qui ne devrait jamais se produire" se produit sur une version déployée, je voudrais quand même en être informé [plus, l'application peut entrer dans un état corrompu, donc une exception est appropriée, je ne veux peut-être pas continuer le flux d'exécution normal)
1
Les assertions doivent être utilisées sur les invariants; des exceptions doivent être utilisées lorsque, par exemple, quelque chose ne doit pas être nul, mais ce sera le cas (comme un paramètre d'une méthode).
Ed
Je suppose que tout dépend de la façon dont vous voulez coder de manière défensive.
Ned Batchelder
Je suis d'accord, pour ce dont vous avez besoin, les exceptions sont la voie à suivre. Vous avez dit que vous aimeriez: les échecs détectés en production, la possibilité de consigner des informations sur les erreurs, et le contrôle du flux d'exécution, etc. Ces trois choses me font penser que ce que vous devez faire est de contourner quelques exceptions.
Tom Neyland
7

Je pense qu'un exemple pratique (artificiel) peut aider à éclairer la différence:

(adapté de l'extension Batch de MoreLinq )

// 'public facing' method
public int DoSomething(List<string> stuff, object doohickey, int limit) {

    // validate user input and report problems externally with exceptions

    if(stuff == null) throw new ArgumentNullException("stuff");
    if(doohickey == null) throw new ArgumentNullException("doohickey");
    if(limit <= 0) throw new ArgumentOutOfRangeException("limit", limit, "Should be > 0");

    return DoSomethingImpl(stuff, doohickey, limit);
}

// 'developer only' method
private static int DoSomethingImpl(List<string> stuff, object doohickey, int limit) {

    // validate input that should only come from other programming methods
    // which we have control over (e.g. we already validated user input in
    // the calling method above), so anything using this method shouldn't
    // need to report problems externally, and compilation mode can remove
    // this "unnecessary" check from production

    Debug.Assert(stuff != null);
    Debug.Assert(doohickey != null);
    Debug.Assert(limit > 0);

    /* now do the actual work... */
}

Donc, comme Eric Lippert et al l'ont dit, vous n'affirmez que des choses que vous vous attendez à être correctes, juste au cas où vous (le développeur) l' auriez mal utilisé ailleurs, afin que vous puissiez corriger votre code. Vous lancez essentiellement des exceptions lorsque vous n'avez aucun contrôle sur ou ne pouvez pas anticiper ce qui arrive, par exemple pour l'entrée de l'utilisateur , de sorte que tout ce qui lui a donné de mauvaises données puisse répondre de manière appropriée (par exemple l'utilisateur).

drzaus
la source
Vos 3 affirmations ne sont-elles pas complètement redondantes? Il est impossible que leurs paramètres soient évalués à faux.
David Klempfner
1
C'est le point - les affirmations sont là pour documenter des choses qui sont impossibles. Pourquoi ferais-tu ça? Parce que vous pourriez avoir quelque chose comme ReSharper qui vous avertit dans la méthode DoSomethingImpl que "vous pourriez déréférencer null ici" et vous voulez lui dire "Je sais ce que je fais, cela ne peut jamais être nul". C'est aussi une indication pour certains programmeurs ultérieurs, qui pourraient ne pas réaliser immédiatement la connexion entre DoSomething et DoSomethingImpl, surtout s'ils sont séparés par des centaines de lignes.
Marcel Popescu
4

Un autre pépite de Code Complete :

"Une assertion est une fonction ou une macro qui se plaint bruyamment si une hypothèse n'est pas vraie. Utilisez des assertions pour documenter les hypothèses faites dans le code et pour éliminer les conditions inattendues. ...

«Pendant le développement, les assertions éliminent les hypothèses contradictoires, les conditions inattendues, les mauvaises valeurs transmises aux routines, etc.

Il ajoute quelques lignes directrices sur ce qui devrait et ne devrait pas être affirmé.

En revanche, les exceptions:

"Utilisez la gestion des exceptions pour attirer l'attention sur les cas inattendus. Les cas exceptionnels doivent être traités de manière à les rendre évidents pendant le développement et récupérables lorsque le code de production est en cours d'exécution."

Si vous n'avez pas ce livre, vous devriez l'acheter.

Andrew Cowenhoven
la source
2
J'ai lu le livre, c'est excellent. Cependant .. vous n'avez pas répondu à ma question :)
Vous avez raison, je n'y ai pas répondu. Ma réponse est non, je ne suis pas d'accord avec vous. Les assertions et exceptions sont des animaux différents comme indiqué ci-dessus et certaines des autres réponses publiées ici.
Andrew Cowenhoven
0

Debug.Assert par défaut ne fonctionnera que dans les versions de débogage, donc si vous voulez détecter tout type de mauvais comportement inattendu dans vos versions de version, vous devrez utiliser des exceptions ou activer la constante de débogage dans les propriétés de votre projet (ce qui est pris en compte dans général de ne pas être une bonne idée).

Mez
la source
la première phrase partielle est vraie, le reste est en général une mauvaise idée: les assertions sont des hypothèses et aucune validation (comme indiqué ci-dessus), l'activation du débogage dans la version n'est vraiment pas une option.
Marc Wittke
0

Utilisez des assertions pour des choses qui SONT possibles mais qui ne devraient pas arriver (si c'était impossible, pourquoi mettriez-vous une assertion?).

Cela ne ressemble-t-il pas à un cas à utiliser Exception? Pourquoi utiliseriez-vous une assertion au lieu d'un Exception?

Parce qu'il devrait y avoir du code qui est appelé avant votre assertion qui empêcherait le paramètre de l'assertion d'être faux.

Habituellement, il n'y a pas de code avant votre Exceptionqui garantit qu'il ne sera pas lancé.

Pourquoi est-ce bon qui Debug.Assert()est compilé en prod? Si vous voulez en savoir plus sur le débogage, ne voudriez-vous pas le savoir en prod?

Vous ne le souhaitez que pendant le développement, car une fois que vous trouvez des Debug.Assert(false)situations, vous écrivez du code pour garantir que Debug.Assert(false)cela ne se reproduira plus. Une fois le développement terminé, en supposant que vous ayez trouvé les Debug.Assert(false)situations et les avoir corrigées, le Debug.Assert()peut être compilé en toute sécurité car ils sont maintenant redondants.

David Klempfner
la source
0

Supposons que vous soyez membre d'une équipe assez importante et que plusieurs personnes travaillent toutes sur la même base de code générale, y compris le chevauchement sur les classes. Vous pouvez créer une méthode qui est appelée par plusieurs autres méthodes et pour éviter les conflits de verrouillage, vous ne lui ajoutez pas de verrou séparé, mais "supposez" qu'elle a été précédemment verrouillée par la méthode appelante avec un verrou spécifique. Tels que, Debug.Assert (RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); Les autres développeurs peuvent ignorer un commentaire indiquant que la méthode appelante doit utiliser le verrou, mais ils ne peuvent pas l'ignorer.

Daniel
la source