Maison > Article > développement back-end > Principes de conception des exceptions .NET
Les exceptions sont des problèmes inévitables lors de l'utilisation de .NET, mais trop de développeurs ne considèrent pas ce problème du point de vue de la conception d'API. Dans la plupart des tâches, ils savent du début à la fin quelles exceptions doivent être interceptées et quelles exceptions doivent être écrites dans le journal global. Si vous concevez une API qui vous permet d'utiliser correctement les exceptions, vous pouvez réduire considérablement le temps nécessaire à la correction des défauts.
La faute à qui ?
La théorie de base derrière la conception d'exceptions commence par la question « À qui la faute ? » Pour les besoins de cet article, la réponse à cette question sera toujours l'une des trois suivantes :
Bibliothèque
Application
Environnement
Quand on dit qu'il y a un problème avec une "bibliothèque", on veut dire qu'une des méthodes actuellement exécutées possède une faille interne. Dans ce cas, "l'application" est le code qui appelle les méthodes de la bibliothèque (c'est un peu déroutant car la bibliothèque et le code de l'application peuvent être dans le même assembly.) Enfin, "l'environnement" fait référence à tout ce qui se trouve en dehors de l'application. ne peut pas être contrôlé.
Défaut de bibliothèque
Le défaut de bibliothèque le plus courant est NullReferenceException. Il n’y a aucune raison pour qu’une bibliothèque lève une exception de référence nulle pouvant être détectée par l’application. Si une valeur nulle est rencontrée, le code de la bibliothèque doit toujours lever une exception plus spécifique décrivant ce qu'est la valeur nulle et comment corriger le problème. Pour les paramètres, il s'agit évidemment d'une ArgumentNullException. Et si la propriété ou le champ est vide, InvalidOperationException est généralement plus approprié.
Par définition, toute exception indiquant un défaut de bibliothèque est un bug dans cette bibliothèque qui doit être corrigé. Cela ne veut pas dire que le code de l'application est exempt de bogues, mais que les bogues de la bibliothèque doivent d'abord être corrigés. Ce n’est qu’à ce moment-là que le développeur de l’application peut savoir qu’il a lui aussi commis une erreur.
La raison en est que de nombreuses personnes peuvent utiliser la même bibliothèque. Si une personne passe par erreur null là où elle ne devrait pas, d'autres feront forcément la même erreur. En remplaçant NullReferenceException par une exception qui montre clairement ce qui n'a pas fonctionné, les développeurs d'applications sauront immédiatement ce qui n'a pas fonctionné.
« Le gouffre du succès »
Si vous lisez les premiers ouvrages sur les modèles de conception .NET, vous rencontrerez souvent l'expression « Le gouffre du succès ». L'idée de base est la suivante : rendre le code facile à utiliser correctement, difficile à utiliser incorrectement, et assurez-vous que les exceptions peuvent vous indiquer ce qui n'a pas fonctionné. En suivant cette philosophie de conception d’API, les développeurs sont presque assurés d’écrire du code correct dès le début.
C'est pourquoi une NullReferenceException non commentée est si mauvaise. À part une trace de pile (qui peut être très profonde dans le code de la bibliothèque), il n'y a aucune information pour aider le développeur à déterminer ce qu'il fait de mal. ArgumentNullException et InvalidOperationException, en revanche, permettent aux auteurs de bibliothèques d'expliquer aux développeurs d'applications comment résoudre le problème.
Autres défauts de bibliothèque
Le prochain défaut de bibliothèque est la série ArithmeticException, comprenant DivideByZeroException, FiniteNumberException et OverflowException. Encore une fois, cela signifie toujours un défaut interne dans la méthode de la bibliothèque, même si ce défaut n'est qu'un contrôle de validité de paramètre manquant.
Un autre exemple de faille de bibliothèque est IndexOutOfRangeException. Sémantiquement, ce n'est pas différent d'ArgumentOutOfRangeException, voir IList.Item, mais cela ne fonctionne qu'avec les indexeurs de tableaux. Étant donné que le code d'application n'utilise généralement pas de tableaux nus, cela signifie que les classes de collection personnalisées auront des bogues.
ArrayTypeMismatchException est rare depuis l'introduction des listes génériques dans .NET 2.0. Les circonstances qui déclenchent cette exception sont plutôt étranges. Selon la documentation :
ArrayTypeMismatchException est levée lorsque le système ne peut pas convertir un élément du tableau en type de tableau déclaré. Par exemple, un élément de type String ne peut pas être stocké dans un tableau de Int32 car il n'y a pas de conversion entre les deux types. Les applications n’ont généralement pas besoin de lever de telles exceptions.
Pour ce faire, le tableau Int32 mentionné précédemment doit être stocké dans une variable de type Object[]. Si vous avez utilisé un tableau brut, la bibliothèque doit le vérifier. Pour cette raison et bien d’autres considérations, il est préférable de ne pas utiliser de tableaux bruts mais de les encapsuler dans une classe de collection appropriée.
En règle générale, d'autres problèmes de diffusion sont reflétés par les exceptions InvalidCastException. De retour à notre sujet, la vérification de type devrait signifier qu'une InvalidCastException n'est jamais levée, mais qu'une ArgumentException ou InvalidOperationException est levée à l'appelant.
MemberAccessException est une classe de base qui couvre diverses erreurs basées sur la réflexion. En plus de l'utilisation directe de la réflexion, l'interopérabilité COM et l'utilisation incorrecte de mots-clés dynamiques peuvent déclencher cette exception.
Défauts de l'application
Les défauts d'application typiques sont ArgumentException et ses sous-classes ArgumentNullException et ArgumentOutOfRangeException. Voici d'autres sous-classes que vous ne connaissez peut-être pas :
System.ComponentModel.InvalidAsynchronousStateException
System.ComponentModel.InvalidEnumArgumentException
System.DuplicateWaitObjectException
System.Globalization.CultureNotFoundException
System.IO.Log.ReservationNotFoundException
System.Text.DecoderFallbackException
System.Text.EncoderFallbackException
Tous ces éléments indiquent clairement que l'application existe est une erreur et le problème réside dans la ligne qui appelle la méthode bibliothèque. Les deux parties de cette déclaration sont importantes. Considérez le code suivant :
foo.Customer = null; foo.Save();
Si le code ci-dessus renvoie une ArgumentNullException, le développeur de l'application sera confus. Il doit lancer une InvalidOperationException indiquant que quelque chose s'est mal passé avant la ligne actuelle.
Documentation des exceptions
Le programmeur typique ne lit pas la documentation, du moins pas en premier lieu. Au lieu de cela, il lit l'API publique, écrit du code et l'exécute. Si le code ne s'exécute pas correctement, recherchez des informations sur les exceptions sur Stack Overflow. Si le programmeur a de la chance, il est facile d'y trouver la réponse ainsi qu'un lien vers la documentation appropriée. Mais même dans ce cas, les programmeurs ne le liront probablement pas.
Alors, en tant qu'auteurs de bibliothèques, comment pouvons-nous résoudre ce problème ? La première étape consiste à copier le document partiel directement dans l'exception.
Plus d'exceptions de statut d'objet
InvalidOperationException a une sous-classe bien connue ObjectDisposedException. Son objectif est évident, cependant, peu de classes destructibles oublient de lever cette exception. En cas d'oubli, un résultat courant est une NullReferenceException. Cette exception est provoquée par la méthode Dispose définissant l’objet enfant destructible sur null.
Étroitement liée à InvalidOperationException est l'exception NotSupportedException. Les deux exceptions sont faciles à distinguer : InvalidOperationException signifie "vous ne pouvez pas faire ça maintenant", tandis que NotSupportedException signifie "vous ne pouvez jamais faire cette opération sur cette classe". En théorie, NotSupportedException ne devrait se produire que lors de l'utilisation d'interfaces abstraites.
Par exemple, une collection immuable doit lever NotSupportedException lorsqu'elle rencontre la méthode IList.Add. En revanche, une collection freezable lèvera une InvalidOperationException lorsqu’elle rencontrera cette méthode à l’état gelé.
Une sous-classe de plus en plus importante de NotSupportedException est PlatformNotSupportedException. Cette exception indique que l'opération peut être effectuée dans certains environnements d'exploitation mais pas dans d'autres. Par exemple, vous devrez peut-être utiliser cette exception lors du portage du code de .NET vers UWP ou .NET Core, car ils ne fournissent pas toutes les fonctionnalités du .NET Framework.
L'insaisissable FormatException
Microsoft a commis quelques erreurs lors de la conception de la première version de .NET. Par exemple, logiquement FormatException est un type d'exception de paramètre, même la documentation dit "Cette exception est levée lorsque le format du paramètre n'est pas valide". Cependant, pour une raison quelconque, il n’hérite pas réellement d’ArgumentException. Il n'a pas non plus d'endroit pour stocker les noms de paramètres.
Notre suggestion temporaire n'est pas de lancer FormatException, mais de créer vous-même une sous-classe d'ArgumentException, qui peut être nommée "ArgumentFormatException" ou d'autres noms avec des effets similaires. Cela peut vous fournir les informations nécessaires telles que les noms des paramètres et les valeurs réelles utilisées, réduisant ainsi le temps de débogage.
Cela nous ramène au thème original du "Design Exceptionnel". Oui, vous pouvez simplement lancer une FormatException lorsque votre analyseur local détecte un problème, mais cela n'aidera pas les développeurs d'applications qui souhaitent utiliser votre bibliothèque.
Un autre exemple de ce défaut de conception du framework est IndexOutOfRangeException. Sémantiquement, ce n'est pas différent d'ArgumentOutOfRangeException, cependant, ce cas particulier est-il réservé aux indexeurs de tableaux ? Non, ce serait une erreur de penser ainsi. En regardant l’ensemble d’instances de IList.Item, cette méthode lancera uniquement ArgumentOutOfRangeException.
Défauts environnementaux
Les défauts environnementaux proviennent du fait que le monde n'est pas parfait, comme les temps d'arrêt des données, le manque de réponse du serveur Web, la perte de fichiers, etc. Lorsqu'un défaut environnemental apparaît dans un rapport de bug, il y a deux aspects à considérer :
L'application a-t-elle géré le défaut correctement ?
Quelles sont les causes des défauts dans cet environnement ?
En général, cela implique une division du travail. Premièrement, les développeurs d’applications devraient être les premiers à chercher des réponses à leurs questions. Cela ne signifie pas seulement gérer les erreurs et récupérer, mais également générer un journal utile.
你可能想知道,为什么要从应用程序开发人员开始。应用程序开发人员要对运维团队负责。如果一次Web服务器调用失败,则应用程序开发人员不能只是甩手大叫“不是我的问题”。他或她首先需要确保异常提供了足够的细节信息,让运维人员可以开展他们的工作。如果异常仅仅提供了“服务器连接超时”的信息,那么他们怎么能知道涉及了哪台服务器?
专用异常
NotImplementedException
NotImplementedException表示且仅表示一件事:这项特性还在开发过程中。因此,NotImplementedException提供的信息应该总是包含一个任务跟踪软件的引用。例如:
throw new NotImplementedException("参见工单#42.");
你可以提供更详细的信息,但实际上,你记录的任何信息几乎立刻就会过期。因此,最好是只将读者导向工单,他们可以在那里看到诸如该特性按计划将会在何时实现这样的信息。
AggregateException
AggregateException是必要之恶,但很难使用。它本身不包含任何有价值的信息,所有的细节信息都隐藏在它的InnerExceptions集合中。
由于AggregateException通常只包含一个项,所以在库中将它解封装并返回真正的异常似乎是合乎逻辑的。一般来说,你不能在没有销毁原始堆栈跟踪的情况下再次抛出一个内部异常,但从.NET 4.5开始,该框架提供了使用ExceptionDispatchInfo的方法。
解封装AggregateException
catch (AggregateException ex) { if (ex.InnerExceptions.Count == 1) //解封装 ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); else throw; //我们真的需要AggregateException }
无法回答的情况
有一些异常无法简单地纳入这个主题。例如,AccessViolationException表示读取非托管内存时有问题。对,那可能是由原生库代码所导致的,也可能是由应用程序错误地使用了同样的代码库所导致的。只有通过研究才能揭示这个Bug的本质。
如果可能,你就应该在设计时避免无法回答的异常。在某些情况下,Visual Studio的静态代码分析器甚至可以分析该规则所涵盖的标识冲突。
例如,ApplicationException实际上已经废弃。Framework设计指南明确指出,“不要抛出或继承ApplicationException。”为此,应用程序不必抛出ApplicationException异常。虽说初衷如此,但看下下面这些子类:
Microsoft.JScript.BreakOutOfFinally
Microsoft.JScript.ContinueOutOfFinally
Microsoft.JScript.JScriptException
Microsoft.JScript.NoContextException
Microsoft.JScript.ReturnOutOfFinally
System.Reflection.InvalidFilterCriteriaException
System.Reflection.TargetException
System.Reflection.TargetInvocationException
System.Reflection.TargetParameterCountException
System.Threading.WaitHandleCannotBeOpenedException
显然,这些子类中有一些应该是参数异常,而其他的则表示环境问题。它们全都不是“应用程序异常”,因为他们只会被.NET Framework的库抛出。
同样的道理,开发人员不应该直接使用SystemException。同ApplicationException一样,SystemException的子类也是各不相同,包括ArgumentException、NullReferenceException和AccessViolationException。微软甚至建议忘掉SystemException的存在,而只使用其子类。
无法回答的情况有一个子类别,就是基础设施异常。我们已经看过AccessViolationException,以下是其他的基础设施异常:
CannotUnloadAppDomainException
BadImageFormatException
DataMisalignedException
TypeLoadException
TypeUnloadedException
这些异常通常很难诊断,可能会揭示出库或调用它的代码中存在的难以理解的Bug。因此,和ApplicationException不同,把它们归为无法回答的情况是合理的。
实践:重新设计SqlException
请记住这些原则,让我们看下SqlException。除了网络错误(你根本无法到达服务器)外,在SQL Server的master.dbo.sysmessages表中有超过11000个不同的错误代码。因此,虽然该异常包含了你需要的所有底层信息,但是,除了简单地捕获&记录外,你实际上难以做任何事。
如果我们要重新设计SqlException,那么我们会希望,根据我们期望用户或开发人员做什么,将其分解成多个不同的类别。
SqlClient.NetworkException会表示所有说明数据库服务器本身之外的环境存在问题的错误代码。
SqlClient.InternalException contiendra un code d'erreur indiquant une panne grave du serveur (telle qu'une corruption de la base de données ou une incapacité d'accéder au disque dur).
SqlClient.SyntaxException est équivalent à notre ArgumentException. Cela signifie que vous transmettez du mauvais SQL au serveur (soit directement, soit à cause d'un bug ORM).
SqlClient.MissingObjectException se produit lorsque la syntaxe est correcte mais que l'objet de la base de données (table, vue, procédure stockée, etc.) n'existe pas.
SqlClient.DeadlockException se produit lorsque deux processus ou plus entrent en conflit lors de la tentative de modification des mêmes informations.
Chacune de ces anomalies implique un plan d'action.
SqlClient.NetworkException : réessayez l'opération. Si cela se produit fréquemment, veuillez contacter le personnel d'exploitation et de maintenance.
SqlClient.InternalException : contactez immédiatement le DBA.
SqlClient.SyntaxException : avertit le développeur de l'application ou de la base de données.
SqlClient.MissingObjectException : veuillez demander au personnel d'exploitation et de maintenance de vérifier si quelque chose a été perdu lors du dernier déploiement de la base de données.
SqlClient.DeadlockException : réessayez l'opération. Si cela se produit fréquemment, recherchez les erreurs de conception.
Si nous devions faire cela dans un travail réel, nous devrions mapper les plus de 11 000 codes d'erreur SQL Server à l'une de ces catégories, ce qui est une tâche particulièrement ardue, qui explique pourquoi SqlException est ce qu'elle est maintenant.
Résumé
Lors de la conception d'une API, afin de faciliter la correction des problèmes, les exceptions doivent être organisées en fonction du type d'action à effectuer. Cela facilite l'écriture de code auto-corrigé, la tenue de journaux plus précis et la communication plus rapide des problèmes à la bonne personne ou à la bonne équipe.
À propos de l'auteur
Jonathan Allen a commencé à participer à des projets MIS pour les cabinets médicaux à la fin des années 1990, les élevant progressivement d'Access et Excel à un niveau d'entreprise solution. Il a passé cinq ans à coder des systèmes de trading automatisés pour le secteur financier avant de décider de se lancer dans le développement d'interfaces utilisateur haut de gamme. Dans ses temps libres, il aime étudier et écrire sur les techniques de combat occidentales du XVe au XVIIe siècle.
Ce qui précède est le contenu des principes de conception d'exceptions .NET. Pour plus de contenu connexe, veuillez faire attention au site Web PHP chinois (www.php.cn) !