Considérations relatives à la gestion des erreurs

31

Le problème:

Depuis longtemps, je m'inquiète du exceptionsmécanisme, car je pense qu'il ne résout pas vraiment ce qu'il devrait.

RÉCLAMATION: Il y a de longs débats à l'extérieur sur ce sujet, et la plupart d'entre eux ont du mal à comparer exceptionsvs renvoyer un code d'erreur. Ce n'est définitivement pas le sujet ici.

Essayer de définir une erreur, je suis d'accord avec CppCoreGuidelines, de Bjarne Stroustrup & Herb Sutter

Une erreur signifie que la fonction ne peut pas atteindre son objectif annoncé

RÉCLAMATION: Le exceptionmécanisme est un langage sémantique pour gérer les erreurs.

RÉCLAMATION: Pour moi, il n'y a "aucune excuse" à une fonction pour ne pas accomplir une tâche: Soit nous avons mal défini les conditions pré / post afin que la fonction ne puisse pas garantir des résultats, soit un cas exceptionnel spécifique n'est pas considéré comme suffisamment important pour passer du temps à développer une solution. Considérant que, l'OMI, la différence entre le code normal et la gestion du code d'erreur est (avant la mise en œuvre) une ligne très subjective.

RÉCLAMATION: L'utilisation d'exceptions pour indiquer quand une condition pré ou post n'est pas conservée est un autre objectif du exceptionmécanisme, principalement à des fins de débogage. Je ne cible pas cette utilisation d' exceptionsici.

Dans de nombreux livres, tutoriels et autres sources, ils ont tendance à montrer que la gestion des erreurs est une science assez objective, qui est résolue exceptionset que vous avez juste besoin d' catcheux pour avoir un logiciel robuste, capable de récupérer de n'importe quelle situation. Mais mes quelques années en tant que développeur me font voir le problème d'une approche différente:

  • Les programmeurs ont tendance à simplifier leur tâche en lançant des exceptions lorsque le cas spécifique semble trop rare pour être mis en œuvre avec soin. Les cas typiques sont les suivants: problèmes de mémoire insuffisante, problèmes de saturation de disque, problèmes de fichiers corrompus, etc. Cela peut être suffisant, mais n'est pas toujours décidé au niveau architectural.
  • Les programmeurs ont tendance à ne pas lire attentivement la documentation sur les exceptions dans les bibliothèques, et ne savent généralement pas à quel moment et quand une fonction est lancée. De plus, même quand ils le savent, ils ne les gèrent pas vraiment.
  • Les programmeurs ont tendance à ne pas détecter les exceptions assez tôt, et lorsqu'ils le font, c'est surtout pour se connecter et lancer plus loin. (se référer au premier point).

Cela a deux conséquences:

  1. Les erreurs qui se produisent fréquemment sont détectées tôt dans le développement et déboguées (ce qui est bien).
  2. De rares exceptions ne sont pas gérées et font planter le système (avec un joli message de log) au domicile de l'utilisateur. Parfois, l'erreur est signalée, ou même pas.

Considérant que l'OMI, l'objectif principal d'un mécanisme d'erreur devrait être:

  1. Rendre visible dans le code où certains cas spécifiques ne sont pas gérés.
  2. Communiquez le runtime du problème au code associé (au moins à l'appelant) lorsque cette situation se produit.
  3. Fournit des mécanismes de récupération

L' exceptionIMO est le principal défaut de la sémantique en tant que mécanisme de gestion des erreurs: il est facile de voir où se throwtrouve a dans le code source, mais il n'est absolument pas évident de savoir si une fonction spécifique pourrait se lancer en regardant la déclaration. Cela apporte tout le problème que j'ai présenté ci-dessus.

Le langage n'applique pas et ne vérifie pas le code d'erreur aussi strictement qu'il le fait pour d'autres aspects du langage (par exemple, des types de variables forts)

Un essai de solution

Dans le but d'améliorer cela, j'ai développé un système de gestion des erreurs très simple, qui essaie de mettre la gestion des erreurs au même niveau d'importance que le code normal.

L'idée est:

  • Chaque fonction (pertinente) reçoit une référence à un successobjet très léger et peut lui attribuer un état d'erreur au cas où. L'objet est très léger jusqu'à ce qu'une erreur de texte soit enregistrée.
  • Une fonction est encouragée à ignorer sa tâche si l'objet fourni contient déjà une erreur.
  • Une erreur ne doit jamais être annulée.

Le design complet prend évidemment en considération chaque aspect (environ 10 pages), ainsi que la façon de l'appliquer à la POO.

Exemple de Successclasse:

class Success
{
public:
    enum SuccessStatus
    {
        ok = 0,             // All is fine
        error = 1,          // Any error has been reached
        uninitialized = 2,  // Initialization is required
        finished = 3,       // This object already performed its task and is not useful anymore
        unimplemented = 4,  // This feature is not implemented already
    };

    Success(){}
    Success( const Success& v);
    virtual ~Success() = default;
    virtual Success& operator= (const Success& v);

    // Comparators
    virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
    virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}

    // Retrieve if the status is not "ok"
    virtual bool operator!() const { return status!=ok;}

    // Retrieve if the status is "ok"
    operator bool() const { return status==ok;}

    // Set a new status
    virtual Success& set( SuccessStatus status, std::string msg="");
    virtual void reset();

    virtual std::string toString() const{ return stateStr;}
    virtual SuccessStatus getStatus() const { return status; }
    virtual operator SuccessStatus() const { return status; }

private:
    std::string stateStr;
    SuccessStatus status = Success::ok;
};

Usage:

double mySqrt( Success& s, double v)
{
    double result = 0.0;
    if (!s) ; // do nothing
    else if (v<0.0) s.set(Error, "Square root require non-negative input.");
    else result = std::sqrt(v);
    return result;
}

Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;

J'ai utilisé cela dans beaucoup de mon (propre) code et cela oblige le programmeur (moi) à réfléchir davantage aux cas exceptionnels possibles et à la façon de les résoudre (bien). Cependant, il a une courbe d'apprentissage et ne s'intègre pas bien avec le code qui l'utilise maintenant.

La question

J'aimerais mieux comprendre les implications de l'utilisation d'un tel paradigme dans un projet:

  • La prémisse du problème est-elle correcte? ou ai-je raté quelque chose de pertinent?
  • La solution est-elle une bonne idée architecturale? ou le prix est trop élevé?

MODIFIER:

Comparaison entre les méthodes:

//Exceptions:

    // Incorrect
    File f = open("text.txt"); // Could throw but nothing tell it! Will crash
    save(f);

    // Correct
    File f;
    try
    {
        f = open("text.txt");
        save(f);
    }
    catch( ... )
    {
        // do something 
    }

//Error code (mixed):

    // Incorrect
    File f = open("text.txt"); //Nothing tell you it may fail! Will crash
    save(f);

    // Correct
    File f = open("text.txt");
    if (f) save(f);

//Error code (pure);

    // Incorrect
    File f;
    open(f, "text.txt"); //Easy to forget the return value! will crash
    save(f);

    //Correct
    File f;
    Error er = open(f, "text.txt");
    if (!er) save(f);

//Success mechanism:

    Success s;
    File f;
    open(s, "text.txt");
    save(s, f); //s cannot be avoided, will never crash.
    if (s) ... //optional. If you created s, you probably don't forget it.
Adrian Maire
la source
25
A voté pour "Cette question montre l'effort de recherche; elle est utile et claire", non pas parce que je suis d'accord: je pense que certaines idées sont erronées. (Les détails peuvent suivre dans une réponse.)
Martin Ba
2
Absolument, je comprends et suis d'accord là-dessus! C'est le but de cette question d'être critiqué. Et le score de la question pour indiquer les bonnes / mauvaises questions, non pas que l'OP a raison.
Adrian Maire
2
Si je comprends bien, votre principal reproche concernant les exceptions est que les gens peuvent l'ignorer (en c ++) au lieu de les manipuler. Cependant, votre construction Success a le même défaut de par sa conception. Comme les exceptions, ils l'ignoreront. Pire encore: il est plus verbeux, conduit à des retours en cascade et vous ne pouvez même pas le "rattraper" en amont.
dagnelies
3
Pourquoi ne pas simplement utiliser quelque chose comme des monades? Ils rendent vos erreurs implicites mais ils ne resteront pas silencieux pendant l'exécution. En fait, la première chose que j'ai pensé en regardant votre code était "monades, sympa". Jetez-y un œil.
bash0r
2
La principale raison pour laquelle j'aime les exceptions est qu'elles vous permettent de détecter toutes les erreurs inattendues d'un bloc de code donné et de les gérer de manière cohérente. Oui, il n'y a aucune bonne raison pour laquelle le code ne devrait pas effectuer sa tâche - "il y a eu un bogue" est une mauvaise raison mais cela se produit toujours , et quand cela se produit, vous voulez enregistrer la cause et afficher un message, ou réessayer. (J'ai du code qui fait une interaction complexe et redémarrable avec un système distant; si le système distant tombe en panne, je veux le journaliser et réessayer depuis le début)
user253751

Réponses:

32

La gestion des erreurs est peut-être la partie la plus difficile d'un programme.

En général, il est facile de réaliser qu'il existe une condition d'erreur; cependant, il est très difficile de le signaler d'une manière qui ne peut être contournée et de le manipuler correctement (voir les niveaux de sécurité exceptionnels d'Abrahams ).

En C, la signalisation des erreurs se fait par un code retour, qui est isomorphe à votre solution.

Le C ++ a introduit des exceptions en raison de l' insuffisance d'une telle approche; à savoir, cela ne fonctionne que si les appelants se souviennent de vérifier si une erreur s'est produite ou non et échoue horriblement sinon. Chaque fois que vous vous dites "c'est OK tant que chaque fois ..." vous avez un problème; les humains ne sont pas méticuleux, même lorsqu'ils s'en soucient.

Le problème, cependant, est que les exceptions ont leurs propres problèmes. À savoir, flux de contrôle invisible / caché. Cela était prévu: masquer le cas d'erreur afin que la logique du code ne soit pas obscurcie par le passe-partout de gestion des erreurs. Cela rend le "chemin heureux" beaucoup plus clair (et rapide!), Au prix de rendre les chemins d'erreur presque impénétrables.


Je trouve intéressant de voir comment d'autres langues abordent le problème:

  • Java a vérifié les exceptions (et les non vérifiées),
  • Go utilise des codes d'erreur / paniques,
  • Rust utilise des types de somme / paniques).
  • Langues FP en général.

C ++ avait l'habitude d'avoir une certaine forme d'exceptions vérifiées, vous avez peut-être remarqué qu'il a été déprécié et simplifié vers un élément de base à la noexcept(<bool>)place: soit une fonction est déclarée comme pouvant lever, soit elle est déclarée comme jamais. Les exceptions vérifiées sont quelque peu problématiques en ce qu'elles manquent d'extensibilité, ce qui peut entraîner des mappages / imbrications maladroits. Et les hiérarchies d'exceptions alambiquées (l'un des principaux cas d'utilisation de l'héritage virtuel est les exceptions ...).

En revanche, Go et Rust adoptent l'approche qui:

  • les erreurs doivent être signalées dans la bande,
  • l'exception doit être utilisée pour des situations vraiment exceptionnelles.

Cette dernière est assez évidente en ce que (1) ils nomment leurs exceptions paniques et (2) il n'y a pas ici de hiérarchie de type / clause compliquée. Le langage n'offre pas la possibilité d'inspecter le contenu d'une "panique": pas de hiérarchie de types, pas de contenu défini par l'utilisateur, juste un "oups, les choses se sont tellement mal passées qu'il n'y a pas de récupération possible".

Cela encourage efficacement les utilisateurs à utiliser une gestion des erreurs appropriée, tout en laissant un moyen facile de renflouer dans des situations exceptionnelles (telles que: "attendez, je n'ai pas encore implémenté cela!").

Bien sûr, l'approche Go ressemble malheureusement beaucoup à la vôtre en ce que vous pouvez facilement oublier de vérifier l'erreur ...

... l'approche Rust est cependant principalement centrée sur deux types:

  • Option, qui est similaire à std::optional,
  • Result, qui est une variante à deux possibilités: Ok et Err.

c'est beaucoup plus net car il n'y a aucune possibilité d'utiliser accidentellement un résultat sans avoir vérifié son succès: si vous le faites, le programme panique.


Les langages FP forment leur gestion des erreurs dans des constructions qui peuvent être divisées en trois couches: - Functor - Applicative / Alternative - Monads / Alternative

Jetons un coup d'œil à la Functorclasse de types de Haskell :

class Functor m where
  fmap :: (a -> b) -> m a -> m b

Tout d'abord, les classes de caractères sont quelque peu similaires mais pas égales aux interfaces. Les signatures de fonction de Haskell semblent un peu effrayantes à première vue. Mais déchiffrons-les. La fonction fmapprend une fonction comme premier paramètre qui est quelque peu similaire à std::function<a,b>. La prochaine chose est un m a. Vous pouvez imaginer mquelque chose comme std::vectoret m aquelque chose comme std::vector<a>. Mais la différence est que m acela ne veut pas dire que cela doit être explicite std:vector. Cela pourrait donc std::optionaussi être un . En indiquant au langage que nous avons une instance pour la classe Functorde types pour un type spécifique comme std::vectorou std::option, nous pouvons utiliser la fonction fmappour ce type. La même chose doit être faite pour les classes de caractères Applicative, AlternativeetMonadce qui vous permet de faire des calculs avec échec possibles et avec état. La Alternativeclasse de types implémente des abstractions de récupération d'erreur. Par cela, vous pouvez dire quelque chose comme a <|> bsignifiant que ce soit un terme aou un terme b. Si aucun des deux calculs ne réussit, c'est toujours une erreur.

Jetons un coup d'œil au Maybetype de Haskell .

data Maybe a
  = Nothing
  | Just a

Cela signifie que lorsque vous attendez un Maybe a, vous obtenez soit Nothingou Just a. En regardant fmapd'en haut, une implémentation pourrait ressembler à

fmap f m = case m of
  Nothing -> Nothing
  Just a -> Just (f a)

L' case ... ofexpression est appelée correspondance de motifs et ressemble à ce que l'on appelle dans le monde de la POO visitor pattern. Imaginez la ligne au case m offur m.apply(...)et à mesure que les points sont l'instanciation d'une classe implémentant les fonctions de répartition. Les lignes sous l' case ... ofexpression sont les fonctions de répartition respectives qui amènent les champs de la classe directement dans la portée par leur nom. Dans la Nothingbranche que nous créons Nothinget dans la Just abranche nous nommons notre seule valeur aet en créons une autre Just ...avec la fonction de transformation fappliquée a. Lisez aussi: new Just(f(a)).

Cela peut désormais gérer les calculs erronés tout en faisant abstraction des vérifications d'erreur réelles. Il existe des implémentations pour les autres interfaces ce qui rend ce type de calcul très puissant. En fait, Maybec'est l'inspiration pour le type de Rust Option.


Je vous y encouragerais plutôt à retravailler votre Successclasse vers un Result. Alexandrescu a en fait proposé quelque chose de très proche, appelé expected<T>, pour lequel des propositions standard ont été faites .

Je m'en tiendrai à la dénomination et à l'API de Rust simplement parce que ... c'est documenté et ça marche. Bien sûr, Rust a un ?opérateur de suffixe astucieux qui rendrait le code beaucoup plus doux; en C ++, nous utiliserons la TRYmacro et l' expression des instructions de GCC pour l'émuler.

template <typename E>
struct Error {
    Error(E e): error(std::move(e)) {}

    E error;
};

template <typename E>
Error<E> error(E e) { return Error<E>(std::move(e)); }

template <typename T, typename E>
struct [[nodiscard]] Result {
    template <typename U>
    Result(U u): ok(true), data(std::move(u)), error() {}

    template <typename F>
    Result(Error<F> f): ok(false), data(), error(std::move(f.error)) {}

    template <typename U, typename F>
    Result(Result<U, F> other):
        ok(other.ok), data(std::move(other.data)),  error(std::move(other.error)) {}

    bool ok = false;
    T data;
    E error;
};

#define TRY(Expr_) \
    ({ auto result = (Expr_); \
       if (!result.ok) { return result; } \
       std::move(result.data); })

Remarque: il Results'agit d'un espace réservé. Une bonne mise en œuvre utiliserait l'encapsulation et a union. Il suffit cependant de faire passer le message.

Ce qui me permet d'écrire ( voir en action ):

Result<double, std::string> sqrt(double x) {
    if (x < 0) {
        return error("sqrt does not accept negative numbers");
    }
    return x;
}

Result<double, std::string> double_sqrt(double x) {
    auto y = TRY(sqrt(x));
    return sqrt(y);
}

que je trouve vraiment bien:

  • contrairement à l'utilisation de codes d'erreur (ou de votre Successclasse), oublier de vérifier les erreurs entraînera une erreur d'exécution 1 plutôt qu'un comportement aléatoire,
  • contrairement à l'utilisation des exceptions, il est évident sur le site d'appel quelles fonctions peuvent échouer, il n'y a donc pas de surprise.
  • avec la norme C ++ - 2X, nous pouvons entrer conceptsdans la norme. Cela rendrait ce type de programmation beaucoup plus agréable car nous pourrions laisser le choix sur le type d'erreur. Par exemple, avec une implémentation de std::vectorcomme résultat, nous pourrions calculer toutes les solutions possibles à la fois. Ou nous pourrions choisir d'améliorer la gestion des erreurs, comme vous l'avez proposé.

1 Avec une Resultimplémentation correctement encapsulée ;)


Remarque: contrairement à l'exception, ce poids léger Resultn'a pas de traces, ce qui rend la journalisation moins efficace; il peut être utile d'enregistrer au moins le numéro de fichier / ligne auquel le message d'erreur est généré et d'écrire généralement un message d'erreur riche. Cela peut être aggravé en capturant le fichier / la ligne chaque fois que la TRYmacro est utilisée, en créant essentiellement la trace manuelle ou en utilisant du code et des bibliothèques spécifiques à la plate-forme de manière libbacktraceà répertorier les symboles dans la pile d'appels.


Il y a cependant une grosse mise en garde: les bibliothèques C ++ existantes, et même std, sont basées sur des exceptions. Ce sera une bataille difficile pour utiliser ce style, car toute API de bibliothèque tierce doit être enveloppée dans un adaptateur ...

Matthieu M.
la source
3
Cette macro a l'air ... très fausse. Je suppose qu'il ({...})s'agit d'une extension gcc, mais même ainsi, ne devrait-il pas en être ainsi if (!result.ok) return result;? Votre état apparaît à l'envers et vous faites une copie inutile de l'erreur.
Mooing Duck
@MooingDuck La réponse explique que ({...})c'est l' expression des déclarations de gcc .
jamesdlin
1
Je recommanderais d'utiliser std::variantpour implémenter le Resultsi vous utilisez C ++ 17. De plus, pour obtenir un avertissement si vous ignorez une erreur, utilisez[[nodiscard]]
Justin
2
@Justin: L'utilisation std::variantou non est quelque peu une question de goût étant donné les compromis autour de la gestion des exceptions. [[nodiscard]]est en effet une pure victoire.
Matthieu M.
46

CLAIM: Le mécanisme d'exception est une langue sémantique pour gérer les erreurs

les exceptions sont un mécanisme de flux de contrôle. La motivation de ce mécanisme de contrôle de flux était de séparer spécifiquement la gestion des erreurs du code de non-gestion des erreurs, dans le cas commun où la gestion des erreurs est très répétitive et n'a que peu de pertinence pour la partie principale de la logique.

RÉCLAMATION: Pour moi, il n'y a "aucune excuse" à une fonction pour ne pas accomplir une tâche: Soit nous avons mal défini les conditions pré / post afin que la fonction ne puisse pas garantir des résultats, soit un cas exceptionnel spécifique n'est pas considéré comme suffisamment important pour passer du temps à développer une solution

Considérez: j'essaie de créer un fichier. Le périphérique de stockage est plein.

Maintenant, ce n'est pas un échec à définir mes conditions préalables: vous ne pouvez pas utiliser "il doit y avoir suffisamment de stockage" comme condition préalable en général, car le stockage partagé est soumis à des conditions de concurrence qui rendent cela impossible à satisfaire.

Alors, mon programme devrait-il libérer de l'espace et continuer avec succès, sinon je suis juste trop paresseux pour "développer une solution"? Cela semble franchement absurde. La "solution" pour gérer le stockage partagé est en dehors de la portée de mon programme , et permettre à mon programme d'échouer avec élégance et d'être réexécuté une fois que l'utilisateur a libéré de l'espace ou ajouté du stockage, est correct .


Ce que fait votre classe de réussite est d'entrelacer la gestion des erreurs de manière très explicite avec la logique de votre programme. Chaque fonction doit vérifier, avant de s'exécuter, si une erreur s'est déjà produite, ce qui signifie qu'elle ne devrait rien faire. Chaque fonction de bibliothèque doit être enveloppée dans une autre fonction, avec un argument de plus (et, espérons-le, une transmission parfaite), qui fait exactement la même chose.

Notez également que votre mySqrtfonction doit renvoyer une valeur même si elle a échoué (ou si une fonction précédente avait échoué). Donc, soit vous retournez une valeur magique (comme NaN), soit vous injectez une valeur indéterminée dans votre programme et vous espérez que rien ne l'utilise sans vérifier l'état de réussite que vous avez appliqué à votre exécution.

Pour la justesse - et les performances - il est préférable de laisser le contrôle hors de portée une fois que vous ne pouvez pas progresser. Les exceptions et la vérification d'erreur explicite de style C avec retour anticipé accomplissent toutes les deux cela.


À titre de comparaison, un exemple de votre idée qui fonctionne vraiment est la monade d'erreur dans Haskell. L'avantage par rapport à votre système est que vous écrivez la majeure partie de votre logique normalement, puis l'enveloppez dans la monade qui se charge d'arrêter l'évaluation lorsqu'une étape échoue. De cette façon, le seul code touchant directement le système de gestion des erreurs est le code qui peut échouer (lancer une erreur) et le code qui doit faire face à l'échec (intercepter une exception).

Je ne suis pas sûr que le style monade et l'évaluation paresseuse se traduisent bien en C ++.

Inutile
la source
1
Grâce à votre réponse, elle éclaire le sujet. Je suppose que l'utilisateur ne serait pas d'accord avec le fait and allowing my program to fail gracefully, and be re-runqu'il vient de perdre 2 heures de travail:
Adrian Maire
14
Votre solution signifie que chaque fois que vous créez un fichier, vous devez inviter l'utilisateur à corriger la situation et réessayer. Ensuite, toutes les autres choses qui pourraient mal tourner, vous devez également les corriger localement. À quelques exceptions près, vous interceptez simplement std::exceptionau niveau supérieur de l'opération logique, dites à l'utilisateur «X a échoué en raison d'ex.what ()» et proposez de réessayer l'opération entière quand et si elle est prête.
inutile
13
@AdrianMaire: Le "permettre d'échouer gracieusement et d'être réexécuté" pourrait également être implémenté en tant que showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try. Il s'agit d'une gestion gracieuse d'un problème qui ne peut généralement pas être effectué à partir du code qui détecte que le premier emplacement de stockage est plein.
Bart van Ingen Schenau
3
L'évaluation @Useless Lazy n'a rien à voir avec l'utilisation de la monade Error, comme en témoignent les langages d'évaluation stricts comme Rust, OCaml et F # qui en font tous un usage intensif.
8bittree du
1
@Useless IMO pour un logiciel de qualité, il est logique que «chaque fois que vous créez un fichier, vous devez inviter l'utilisateur à corriger la situation et réessayer». Les premiers programmeurs se sont souvent donné beaucoup de mal pour récupérer les erreurs, au moins le programme TeX de Knuth en est plein. Et avec son framework de «programmation alphabétisée», il a trouvé un moyen de conserver la gestion des erreurs dans une autre section, afin que le code reste lisible et que la récupération d'erreur soit écrite avec plus de soin (car lorsque vous écrivez la section de récupération d'erreur, c'est le point et le programmeur a tendance à faire un meilleur travail).
ShreevatsaR
15

J'aimerais mieux comprendre les implications de l'utilisation d'un tel paradigme dans un projet:

  • La prémisse du problème est-elle correcte? ou ai-je raté quelque chose de pertinent?
  • La solution est-elle une bonne idée architecturale? ou le prix est trop élevé?

Votre approche apporte de gros problèmes dans votre code source:

  • il s'appuie sur le code client en se souvenant toujours de vérifier la valeur de s. Ceci est courant avec l' utilisation des codes retour pour l' approche de gestion des erreurs , et l'une des raisons pour lesquelles des exceptions ont été introduites dans le langage: avec les exceptions, si vous échouez, vous n'échouez pas en silence.

  • plus vous écrivez de code avec cette approche, plus vous devrez ajouter de code passe-partout pour la gestion des erreurs (votre code n'est plus minimaliste) et votre effort de maintenance augmente.

Mais mes quelques années en tant que développeur me font voir le problème d'une approche différente:

Les solutions à ces problèmes doivent être abordées au niveau du responsable technique ou au niveau de l'équipe:

Les programmeurs ont tendance à simplifier leur tâche en lançant des exceptions lorsque le cas spécifique semble trop rare pour être mis en œuvre avec soin. Les cas typiques sont les suivants: problèmes de mémoire insuffisante, problèmes de saturation de disque, problèmes de fichiers corrompus, etc. Cela peut être suffisant, mais n'est pas toujours décidé au niveau architectural.

Si vous vous retrouvez à gérer chaque type d'exception qui peut être levé, tout le temps, alors la conception n'est pas bonne; Les erreurs traitées doivent être décidées en fonction des spécifications du projet, et non en fonction de ce que les développeurs ont envie de mettre en œuvre.

Adresse en mettant en place des tests automatisés, en séparant la spécification des tests unitaires et la mise en œuvre (faire faire deux personnes différentes).

Les programmeurs ont tendance à ne pas lire attentivement la documentation [...] De plus, même lorsqu'ils savent, ils ne les gèrent pas vraiment.

Vous ne réglerez pas cela en écrivant plus de code. Je pense que votre meilleur pari est des revues de code méticuleusement appliquées.

Les programmeurs ont tendance à ne pas détecter les exceptions assez tôt, et lorsqu'ils le font, c'est surtout pour se connecter et lancer plus loin. (se référer au premier point).

La gestion correcte des erreurs est difficile, mais moins fastidieuse avec des exceptions qu'avec des valeurs de retour (qu'elles soient réellement retournées ou passées en tant qu'arguments d'E / S).

La partie la plus délicate de la gestion des erreurs n'est pas la façon dont vous recevez l'erreur, mais comment vous assurer que votre application conserve un état cohérent en présence d'erreurs.

Pour y remédier, une plus grande attention doit être accordée à l'identification et à l'exécution dans des conditions d'erreur (davantage de tests, davantage de tests unitaires / d'intégration, etc.).

utnapistim
la source
12
Tout le code après une erreur est ignoré, si vous vous souvenez de vérifier chaque fois que vous recevez une instance comme argument . C'est ce que j'entendais par «plus vous écrivez de code avec cette approche, plus vous devrez ajouter de code passe-partout d'erreur». Vous devrez énoncer votre code avec des ifs sur l'instance de succès, et chaque fois que vous oubliez, c'est un bug. Le deuxième problème causé par l'oubli de vérifier: le code qui s'exécute jusqu'à ce que vous vérifiiez à nouveau, n'aurait pas dû être exécuté du tout (continuer si vous oubliez de vérifier, corrompt vos données).
utnapistim
11
Non, la gestion d'une exception (ou le renvoi d'un code d'erreur) n'est pas un crash - sauf si l'erreur / l'exception est logiquement fatale ou si vous choisissez de ne pas la gérer. Vous avez toujours la possibilité de gérer le cas d'erreur, sans avoir à vérifier explicitement à chaque étape si une erreur s'est déjà produite
Inutile
11
@AdrianMaire Dans presque toutes les applications sur lesquelles je travaille, je préférerais de loin un crash plutôt que de continuer silencieusement. Je travaille sur des logiciels critiques pour l'entreprise où prendre de mauvaises sorties et continuer à les exploiter pourrait entraîner des pertes considérables. Si l'exactitude est cruciale et que le plantage est acceptable, les exceptions ont ici un très grand avantage.
Chris Hayes
1
@AdrianMaire - Je pense qu'il est beaucoup plus difficile d'oublier de gérer une exception que votre méthode d'oubli d'une instruction if ... En outre - le principal avantage des exceptions est la couche qui les gère. Vous souhaiterez peut-être laisser une exception système se multiplier pour afficher un message d'erreur au niveau de l'application, mais gérer les situations que vous connaissez à un niveau inférieur. Si vous utilisez des bibliothèques tierces ou d'autres développeurs, c'est vraiment le seul choix ...
Milney
5
@Adrian Pas d'erreur, vous semblez avoir mal lu ce que j'ai écrit ou en avoir raté la seconde moitié. Mon argument n'est pas que l'exception se produira pendant les tests / développement et que les développeurs se rendront compte qu'ils doivent les gérer. Le fait est que la conséquence d'une exception complètement non gérée dans la production est préférable à la conséquence d'un code d'erreur non contrôlé. si vous manquez le code d'erreur, vous obtenez et continuez à utiliser des résultats erronés. Si vous manquez l'exception, l'application se bloque et ne continue pas de s'exécuter, vous n'obtenez aucun résultat, pas de mauvais résultats . (suite)
Mr.Mindor