Quand est-il correct pour un constructeur de lancer une exception?

Quand est-il correct pour un constructeur de lancer une exception? (Ou dans le cas de l’objective C: à quel moment un initiateur a-t-il raison de revenir à zéro?)

Il me semble qu’un constructeur devrait échouer – et donc refuser de créer un object – si l’object n’est pas complet. Par exemple, le constructeur doit avoir un contrat avec son appelant pour fournir un object fonctionnel et fonctionnel sur lequel les méthodes peuvent être appelées de manière significative? Est-ce raisonnable?

Le travail du constructeur consiste à amener l’object dans un état utilisable. Il y a essentiellement deux écoles de pensée à ce sujet.

Un groupe favorise la construction en deux étapes. Le constructeur amène simplement l’object dans un état dormant dans lequel il refuse de travailler. Il y a une fonction supplémentaire qui effectue l’initialisation réelle.

Je n’ai jamais compris le raisonnement derrière cette approche. Je suis fermement dans le groupe qui prend en charge la construction en une étape, où l’object est entièrement initialisé et utilisable après la construction.

Les constructeurs à une étape doivent lancer s’ils ne parviennent pas à initialiser complètement l’object. Si l’object ne peut pas être initialisé, il ne doit pas être autorisé à exister, le constructeur doit donc lancer.

Eric Lippert dit qu’il existe 4 types d’exceptions.

  • Les exceptions fatales ne sont pas de votre faute, vous ne pouvez pas les empêcher et vous ne pouvez pas les nettoyer raisonnablement.
  • Les exceptions Boneheaded sont votre propre faute, vous les avez peut-être prévenues et elles sont donc des bogues dans votre code.
  • Les exceptions vexantes sont le résultat de décisions de conception malheureuses. Les exceptions vexantes sont jetées dans des circonstances totalement exceptionnelles et doivent donc être sockets et traitées en permanence.
  • Et finalement, les exceptions exogènes semblent être un peu comme des exceptions frustrantes, sauf qu’elles ne résultent pas de choix de conception malheureux. Plutôt, ils sont le résultat de réalités externes désordonnées qui affectent votre belle logique de programme.

Votre constructeur ne doit jamais lancer une exception fatale, mais le code qu’il exécute peut provoquer une exception fatale. Quelque chose comme “hors mémoire” n’est pas quelque chose que vous pouvez contrôler, mais si cela se produit dans un constructeur, hé, ça arrive.

Les exceptions Boneheaded ne doivent jamais apparaître dans aucun de vos codes, elles sont donc immédiatement disponibles.

Les exceptions Vexing (l’exemple est Int32.Parse() ) ne doivent pas être lancées par des constructeurs, car ils ne présentent pas de circonstances non exceptionnelles.

Enfin, les exceptions exogènes doivent être évitées, mais si vous faites quelque chose dans votre constructeur qui dépend de circonstances externes (comme le réseau ou le système de fichiers), il convient de lancer une exception.

Il n’y a généralement rien à gagner en divorçant l’initialisation d’object de la construction. RAII est correct, un appel réussi au constructeur doit soit aboutir à un object direct entièrement initialisé, soit échouer, et TOUTES les défaillances à tout sharepoint tout chemin de code doivent toujours lancer une exception. Vous ne gagnez rien en utilisant une méthode init () distincte, sauf une complexité supplémentaire à un certain niveau. Le contrat de ctor devrait être soit il retourne un object valide fonctionnel ou il se nettoie après lui et lève.

Considérez, si vous implémentez une méthode init distincte, vous devez toujours l’ appeler. Il a toujours le potentiel de lancer des exceptions, il faut toujours les manipuler et il faut toujours les appeler immédiatement après le constructeur, sauf que maintenant vous avez 4 états d’object possibles au lieu de 2 (IE, construit, initialisé, non initialisé, et échoué vs juste valide et inexistant).

Dans tous les cas, j’ai rencontré 25 ans de cas de développement OO où il semble qu’une méthode d’initialisation distincte «résoudrait un problème» sont des défauts de conception. Si vous n’avez pas besoin d’un object maintenant, vous ne devriez pas le construire maintenant, et si vous en avez besoin maintenant, vous devez l’initialiser. KISS devrait toujours être le principe suivi, avec le concept simple que le comportement, l’état et l’API de n’importe quelle interface devraient refléter ce que fait l’object, pas comment il le fait, le code client ne devrait même pas savoir que l’object a quelque chose de l’état interne qui nécessite une initialisation, donc le modèle init après viole ce principe.

En raison de tous les problèmes que peut causer une classe partiellement créée, je dirais jamais.

Si vous devez valider quelque chose pendant la construction, rendez le constructeur privé et définissez une méthode de fabrication statique publique. La méthode peut lancer si quelque chose est invalide. Mais si tout s’exécute, il appelle le constructeur, qui est assuré de ne pas lancer.

Un constructeur doit lancer une exception lorsqu’il ne peut pas terminer la construction dudit object.

Par exemple, si le constructeur est supposé allouer 1024 Ko de ram, et que cela échoue, il doit lancer une exception, de cette manière l’appelant du constructeur sait que l’object n’est pas prêt à être utilisé et qu’il y a une erreur. quelque part qui doit être réparé.

Les objects à moitié initialisés et à moitié morts posent des problèmes et des problèmes, car l’appelant n’a aucun moyen de le savoir. Je préférerais que mon constructeur lance une erreur lorsque les choses tournent mal, plutôt que de devoir compter sur la programmation pour lancer un appel à la fonction isOK () qui renvoie true ou false.

C’est toujours assez risqué, surtout si vous allouez des ressources à l’intérieur d’un constructeur. selon votre langue, le destructeur ne sera pas appelé, vous devez donc le nettoyer manuellement. Cela dépend de la manière dont la vie d’un object commence dans votre langue.

La seule fois où je l’ai vraiment fait, c’est quand il y a eu un problème de sécurité quelque part, cela signifie que l’object ne doit pas être créé, plutôt que ne peut pas l’être.

Il est raisonnable qu’un constructeur lance une exception tant qu’il se nettoie correctement. Si vous suivez le paradigme RAII (l’acquisition de ressources est l’initialisation), il est assez courant qu’un constructeur effectue un travail significatif; un constructeur bien écrit se nettoie à son tour s’il ne peut pas être complètement initialisé.

Autant que je sache, personne ne présente une solution assez évidente qui incarne le meilleur de la construction en une ou deux étapes.

note: cette réponse suppose que C #, mais les principes peuvent être appliqués dans la plupart des langues.

Premièrement, les avantages des deux:

Une étape

La construction en une étape nous est bénéfique en empêchant les objects d’exister dans un état invalide, évitant ainsi toutes sortes de gestion d’état erronée et tous les bogues qui l’accompagnent. Cependant, certains d’entre nous se sentent bizarres parce que nous ne voulons pas que nos constructeurs lancent des exceptions, et c’est parfois ce que nous devons faire lorsque les arguments d’initialisation ne sont pas valides.

 public class Person { public ssortingng Name { get; } public DateTime DateOfBirth { get; } public Person(ssortingng name, DateTime dateOfBirth) { if (ssortingng.IsNullOrWhitespace(name)) { throw new ArgumentException(nameof(name)); } if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow { throw new ArgumentOutOfRangeException(nameof(dateOfBirth)); } this.Name = name; this.DateOfBirth = dateOfBirth; } } 

Deux étapes via la méthode de validation

La construction en deux étapes nous permet d’exécuter notre validation en dehors du constructeur, évitant ainsi de devoir lancer des exceptions au sein du constructeur. Cependant, cela nous laisse des instances “invalides”, ce qui signifie qu’il y a un état que nous devons suivre et gérer pour l’instance, ou nous le jetons immédiatement après l’allocation de tas. Cela soulève la question suivante: Pourquoi effectuons-nous une allocation de tas, et donc une collecte de mémoire, sur un object que nous n’utilisons même pas?

 public class Person { public ssortingng Name { get; } public DateTime DateOfBirth { get; } public Person(ssortingng name, DateTime dateOfBirth) { this.Name = name; this.DateOfBirth = dateOfBirth; } public void Validate() { if (ssortingng.IsNullOrWhitespace(Name)) { throw new ArgumentException(nameof(Name)); } if (DateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow { throw new ArgumentOutOfRangeException(nameof(DateOfBirth)); } } } 

Single-stage via constructeur privé

Alors, comment pouvons-nous garder les exceptions hors de nos constructeurs et nous empêcher d’effectuer une allocation de tas sur des objects qui seront immédiatement éliminés? C’est assez simple: nous rendons le constructeur privé et créons des instances via une méthode statique désignée pour effectuer une instanciation, et donc une allocation de tas, seulement après validation.

 public class Person { public ssortingng Name { get; } public DateTime DateOfBirth { get; } private Person(ssortingng name, DateTime dateOfBirth) { this.Name = name; this.DateOfBirth = dateOfBirth; } public static Person Create( ssortingng name, DateTime dateOfBirth) { if (ssortingng.IsNullOrWhitespace(Name)) { throw new ArgumentException(nameof(name)); } if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow { throw new ArgumentOutOfRangeException(nameof(DateOfBirth)); } return new Person(name, dateOfBirth); } } 

Async Single-Stage via constructeur privé

Outre les avantages de la validation et de la prévention de l’allocation de mémoire mentionnés ci-dessus, la méthodologie précédente nous offre un autre avantage intéressant: le support asynchrone. Cela s’avère pratique lorsque vous traitez une authentification en plusieurs étapes, par exemple lorsque vous devez récupérer un jeton de support avant d’utiliser votre API. De cette façon, vous ne vous retrouvez pas avec un client API “déconnecté” invalide, et vous pouvez simplement recréer le client API si vous recevez une erreur d’autorisation lors d’une tentative d’exécution d’une requête.

 public class RestApiClient { public RestApiClient(HttpClient httpClient) { this.httpClient = new httpClient; } public async Task Create(ssortingng username, ssortingng password) { if (username == null) { throw new ArgumentNullException(nameof(username)); } if (password == null) { throw new ArgumentNullException(nameof(password)); } var basicAuthBytes = Encoding.ASCII.GetBytes($"{username}:{password}"); var basicAuthValue = Convert.ToBase64Ssortingng(basicAuthBytes); var authenticationHttpClient = new HttpClient { BaseUri = new Uri("https://auth.example.io"), DefaultRequestHeaders = { Authentication = new AuthenticationHeaderValue("Basic", basicAuthValue) } }; using (authenticationHttpClient) { var response = await httpClient.GetAsync("login"); var content = response.Content.ReadAsSsortingngAsync(); var authToken = content; var restApiHttpClient = new HttpClient { BaseUri = new Uri("https://api.example.io"), // notice this differs from the auth uri DefaultRequestHeaders = { Authentication = new AuthenticationHeaderValue("Bearer", authToken) } }; return new RestApiClient(restApiHttpClient); } } } 

Les inconvénients de cette méthode sont peu nombreux, selon mon expérience.

Généralement, utiliser cette méthodologie signifie que vous ne pouvez plus utiliser la classe en tant que DTO, car la désérialisation en un object sans constructeur public par défaut est au mieux difficile. Cependant, si vous utilisiez l’object en tant que DTO, vous ne devriez pas vraiment valider l’object lui-même, mais plutôt invalider les valeurs de l’object lorsque vous tentez de les utiliser, car techniquement les valeurs ne sont pas “invalides” en ce qui concerne au DTO.

Cela signifie également que vous finirez par créer des méthodes ou des classes d’usine lorsque vous devez autoriser un conteneur IOC à créer l’object, car sinon le conteneur ne saura pas comment instancier l’object. Cependant, dans de nombreux cas, les méthodes d’usine sont l’une des méthodes de Create elles-mêmes.

Voir les sections 17.2 et 17.4 de la FAQ C ++.

En général, j’ai trouvé ce code plus facile à porter et à maintenir les résultats si les constructeurs sont écrits pour ne pas échouer, et le code qui peut échouer est placé dans une méthode distincte qui renvoie un code d’erreur et laisse l’object dans un état inerte .

Si vous écrivez des contrôles d’interface utilisateur (ASPX, WinForms, WPF, …), évitez de lancer des exceptions dans le constructeur car le concepteur (Visual Studio) ne peut pas les gérer lorsqu’il crée vos contrôles. Connaissez votre cycle de contrôle (événements de contrôle) et utilisez autant que possible une initialisation paresseuse.

Notez que si vous lancez une exception dans un initialiseur, vous finirez par fuir si un code utilise le [[[MyObj alloc] init] autorelease] , car l’exception ignorera le lancement automatique.

Voir cette question:

Comment empêcher les fuites lors de la levée d’une exception dans init?

Vous devez absolument lancer une exception à partir d’un constructeur si vous ne parvenez pas à créer un object valide. Cela vous permet de fournir des invariants appropriés dans votre classe.

En pratique, vous devrez peut-être faire très attention. Rappelez-vous qu’en C ++, le destructeur ne sera pas appelé, donc si vous lancez après avoir alloué vos ressources, vous devez faire très attention à gérer cela correctement!

Cette page a une discussion approfondie de la situation en C ++.

Lancez une exception si vous ne parvenez pas à initialiser l’object dans le constructeur, par exemple, les arguments illégaux.

En règle générale, une exception doit toujours être levée dès que possible, car cela facilite le débogage lorsque la source du problème est plus proche de la méthode signalant que quelque chose ne va pas.

Lancer une exception pendant la construction est un excellent moyen de rendre votre code plus complexe. Les choses qui sembleraient simples deviennent soudainement difficiles. Par exemple, disons que vous avez une stack. Comment faites-vous pour faire apparaître la stack et retourner la valeur supérieure? Eh bien, si les objects dans la stack peuvent lancer leurs constructeurs (en construisant le temporaire pour retourner à l’appelant), vous ne pouvez pas garantir que vous ne perdrez pas de données (décrémenter le pointeur de la stack, construire une valeur de retour stack, qui lance, et maintenant une stack qui vient de perdre un object)! C’est pourquoi std :: stack :: pop ne renvoie pas de valeur, et vous devez appeler std :: stack :: top.

Ce problème est bien décrit ici , cochez le point 10, en écrivant du code d’exception.

Le contrat habituel dans OO est que les méthodes object fonctionnent réellement.

Donc, comme corrolary, ne jamais retourner un object zombie dans un constructeur / init.

Un zombie n’est pas fonctionnel et peut manquer de composants internes. Juste une exception de pointeur nul devant arriver.

J’ai d’abord fait des zombies en Objective C, il y a de nombreuses années.

Comme toutes les règles de base, il existe une “exception”.

Il est tout à fait possible qu’une interface spécifique ait un contrat indiquant qu’il existe une méthode “initialize” autorisée à faire une exception. Qu’un object incorporant cette interface ne réponde pas correctement aux appels, à l’exception des propriétés, jusqu’à ce que l’initialisation ait été appelée. Je l’ai utilisé pour les pilotes de périphériques dans un système d’exploitation OO au cours du processus de démarrage, et cela était réalisable.

En général, vous ne voulez pas d’objects zombie. Dans des langages comme Smalltalk avec devenu, les choses deviennent un peu pétillantes, mais la surutilisation de devenir est aussi un mauvais style. Become permet à un object de se transformer en un autre object in situ. Il n’est donc pas nécessaire d’utiliser wrapper-wrapper (Advanced C ++) ou le modèle de stratégie (GOF).

Je ne peux pas aborder les meilleures pratiques en Objective-C, mais en C ++, un constructeur peut lancer une exception. D’autant plus qu’il n’y a pas d’autre moyen de s’assurer qu’une condition exceptionnelle rencontrée lors de la construction est rapscope sans avoir recours à une méthode isOK ().

La fonction de fonction try block a été conçue spécifiquement pour prendre en charge les échecs lors de l’initialisation des membres du constructeur (bien qu’elle puisse également être utilisée pour les fonctions normales). C’est le seul moyen de modifier ou d’enrichir les informations d’exception qui seront lancées. Mais en raison de son objective de conception d’origine (utilisation dans les constructeurs), il ne permet pas que l’exception soit avalée par une clause catch () vide.

Oui, si le constructeur ne parvient pas à en créer une partie interne, cela peut être – par choix – sa responsabilité de lancer (et dans certaines langues de déclarer) une exception explicite , dûment notée dans la documentation du constructeur.

Ce n’est pas la seule option: il pourrait finir le constructeur et construire un object, mais avec une méthode ‘isCoherent ()’ renvoyant false, afin de pouvoir signaler un état incohérent (cela peut être préférable dans certains cas, pour éviter une interruption brutale du workflow d’exécution due à une exception)
Attention: comme le dit EricSchaefer dans son commentaire, cela peut apporter une certaine complexité au test unitaire (un lancer peut augmenter la complexité cyclomatique de la fonction à cause de la condition qui le déclenche)

S’il échoue à cause de l’appelant (comme un argument null fourni par l’appelant, où le constructeur appelé attend un argument non nul), le constructeur lancera de toute façon une exception d’exécution non contrôlée.

La question du PO a une balise “non agnostique” … cette question ne peut pas être répondue de la même manière pour toutes les langues / situations.

La hiérarchie de classes de l’exemple C # suivant renvoie au constructeur de la classe B, ignorant un appel immédiat à la classe IDisposeable.Dispose à la sortie de l’utilisation principale, en ignorant la IDisposeable.Dispose explicite des ressources de la classe A.

Si, par exemple, la classe A avait créé un Socket à la construction, connecté à une ressource réseau, cela serait probablement toujours le cas après le bloc using (une anomalie relativement cachée).

 class A : IDisposable { public A() { Console.WriteLine("Initialize A's resources."); } public void Dispose() { Console.WriteLine("Dispose A's resources."); } } class B : A, IDisposable { public B() { Console.WriteLine("Initialize B's resources."); throw new Exception("B construction failure: B can cleanup anything before throwing so this is not a worry."); } public new void Dispose() { Console.WriteLine("Dispose B's resources."); base.Dispose(); } } class C : B, IDisposable { public C() { Console.WriteLine("Initialize C's resources. Not called because B throws during construction. C's resources not a worry."); } public new void Dispose() { Console.WriteLine("Dispose C's resources."); base.Dispose(); } } class Program { static void Main(ssortingng[] args) { try { using (C c = new C()) { } } catch { } // Resource's allocated by c's "A" not explicitly disposed. } } 

En parlant ssortingctement d’un sharepoint vue Java, chaque fois que vous initialisez un constructeur avec des valeurs illégales, il doit déclencher une exception. De cette façon, il ne se construit pas dans un mauvais état.

Pour moi, c’est une décision de conception quelque peu philosophique.

Il est très agréable d’avoir des instances valables tant qu’elles existent, à partir de la date du début. Pour de nombreux cas non sortingviaux, cela peut nécessiter l’envoi d’exceptions de la part de ctor si une allocation mémoire / ressources ne peut pas être effectuée.

Certaines autres approches sont la méthode init () qui comporte certains problèmes. L’un d’entre eux est de s’assurer que init () est effectivement appelé.

Une variante utilise une approche paresseuse pour appeler automatiquement init () la première fois qu’un accesseur / mutateur est appelé, mais cela nécessite que tout appelant potentiel ait à se soucier de la validité de l’object. (Par opposition à “ça existe, donc c’est une philosophie valide”).

J’ai vu plusieurs modèles de conception proposés pour résoudre ce problème. Par exemple, être capable de créer un object initial via ctor, mais avoir à appeler init () pour mettre la main sur un object contenu initialisé avec des accesseurs / mutateurs.

Chaque approche a ses hauts et ses bas; J’ai utilisé tous ces éléments avec succès. Si vous ne fabriquez pas d’objects prêts à l’emploi à partir du moment où ils sont créés, je vous recommande une forte dose d’assertions ou d’exceptions pour vous assurer que les utilisateurs n’interagissent pas avant init ().

Addenda

J’ai écrit du sharepoint vue d’un programmeur C ++. Je suppose également que vous utilisez correctement l’idiome RAII pour gérer les ressources libérées lorsque des exceptions sont levées.

Je ne fais qu’apprendre l’Objectif C, donc je ne peux pas vraiment parler par expérience, mais j’ai lu à ce sujet dans les documents d’Apple.

http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/chapter_3_section_6.html

Non seulement cela vous dira comment gérer la question que vous avez posée, mais cela vous aidera aussi à l’expliquer.

En utilisant des fabriques ou des méthodes d’usine pour toute la création d’objects, vous pouvez éviter les objects non valides sans émettre des exceptions des constructeurs. La méthode de création doit renvoyer l’object demandé s’il est capable d’en créer un, ou null si ce n’est pas le cas. Vous perdez un peu de souplesse dans la gestion des erreurs de construction dans l’utilisateur d’une classe, car le fait de retourner null ne vous dit pas ce qui a mal tourné dans la création de l’object. Mais cela évite également d’append la complexité de plusieurs gestionnaires d’exceptions chaque fois que vous demandez un object, et le risque d’attraper des exceptions que vous ne devez pas gérer.

Le meilleur conseil que j’ai vu sur les exceptions est de lancer une exception si, et seulement si, l’autre consiste à ne pas respecter une condition de publication ou à maintenir un invariant.

Ce conseil remplace une décision subjective imprécise (est-ce une bonne idée ) par une question technique et précise basée sur des décisions de conception (conditions invariantes et postérieures) que vous devriez déjà avoir sockets.

Les constructeurs ne sont qu’un cas particulier, mais pas spécial, pour ce conseil. La question devient donc: quels invariants une classe doit-elle avoir? Les partisans d’une méthode d’initialisation distincte, à appeler après la construction, suggèrent que la classe dispose de deux modes de fonctionnement ou plus, avec un mode non prêt après la construction et au moins un mode prêt entré après l’initialisation. C’est une complication supplémentaire, mais acceptable si la classe dispose de plusieurs modes de fonctionnement de toute façon. Il est difficile de voir comment cette complication vaut la peine si la classe n’aurait pas autrement de mode de fonctionnement.

Notez que le fait de mettre en place une méthode d’initialisation séparée ne vous permet pas d’éviter que des exceptions soient levées. Les exceptions que votre constructeur peut avoir lancées seront désormais renvoyées par la méthode d’initialisation. Toutes les méthodes utiles de votre classe devront générer des exceptions si elles sont appelées pour un object non initialisé.

Notez également qu’éviter la possibilité que des exceptions soient lancées par votre constructeur est gênant et, dans de nombreux cas, impossible dans de nombreuses bibliothèques standard. Cela est dû au fait que les concepteurs de ces bibliothèques pensent que lancer des exceptions auprès des constructeurs est une bonne idée. En particulier, toute opération qui tente d’acquérir une ressource non partageable ou finie (telle que l’allocation de mémoire) peut échouer et cette défaillance est généralement indiquée dans les langages et les bibliothèques OO en lançant une exception.

Je ne suis pas sûr que toute réponse puisse être totalement indépendante du langage. Certaines langues gèrent différemment les exceptions et la gestion de la mémoire.

J’ai déjà travaillé sous des normes de codage exigeant des exceptions à ne jamais être utilisées et uniquement des codes d’erreur sur les initialiseurs, car les développeurs avaient été brûlés par le langage et manipulaient mal les exceptions. Les langages sans la récupération de la mémoire traiteront le tas et la stack très différemment, ce qui peut être important pour les objects non RAII. Il est important qu’une équipe décide d’être cohérente afin de savoir par défaut si elle doit appeler les initialiseurs après les constructeurs. Toutes les méthodes (y compris les constructeurs) doivent également être bien documentées pour savoir quelles exceptions elles peuvent lancer, afin que les appelants sachent comment les gérer.

Je suis généralement en faveur d’une construction en une seule étape, car il est facile d’oublier d’initialiser un object, mais il y a beaucoup d’exceptions à cela.

  • Votre prise en charge de la langue pour les exceptions n’est pas très bonne.
  • Vous avez un motif pressant de continuer à utiliser new et delete
  • Votre initialisation requirejs beaucoup de processeur et doit exécuter une async sur le thread qui a créé l’object.
  • Vous créez une DLL qui peut générer des exceptions en dehors de son interface avec une application utilisant une autre langue. Dans ce cas, il ne s’agit peut-être pas tellement de ne pas lancer d’exceptions, mais de s’assurer qu’elles sont interceptées avant l’interface publique. (Vous pouvez intercepter des exceptions C ++ en C #, mais il existe des obstacles à traverser.)
  • Constructeurs statiques (C #)

Les acteurs ne sont pas censés faire des choses “intelligentes”, donc il est inutile de lancer une exception. Utilisez une méthode Init () ou Setup () si vous souhaitez effectuer une configuration d’object plus compliquée.