web-dev-qa-db-fra.com

Pourquoi les lancers ne sont-ils pas sécurisés dans Swift?

Le plus grand malentendu pour moi dans Swift est le mot clé throws. Considérez le morceau de code suivant:

func myUsefulFunction() throws

Nous ne pouvons pas vraiment comprendre quel genre d'erreur cela provoquera. La seule chose que nous savons, c'est que cela pourrait générer une erreur. La seule façon de comprendre ce que pourrait être l'erreur est de consulter la documentation ou de vérifier l'erreur lors de l'exécution.

Mais n'est-ce pas contraire à la nature de Swift? Swift a des génériques puissants et un système de type pour rendre le code expressif, mais on a l'impression que throws est exactement opposé car vous ne pouvez rien obtenir sur l'erreur en regardant la fonction Signature.

Pourquoi est-ce si? Ou ai-je raté quelque chose d'important et ai-je mal compris le concept?

47
Noobass

Le choix est une décision de conception délibérée.

Ils ne voulaient pas que vous n'ayez pas à déclarer de levée d'exceptions comme en Objective-C, C++ et C # car cela oblige les appelants à assumer toutes les fonctions et à inclure des passe-partout pour gérer les exceptions qui ne se produiraient pas, ou à ignorez simplement la possibilité d'exceptions. Aucune de ces options n'est idéale et la seconde rend les exceptions inutilisables, sauf dans le cas où vous souhaitez mettre fin au programme car vous ne pouvez pas garantir que chaque fonction de la pile d'appels a correctement désalloué les ressources lorsque la pile est déroulée.

L'autre extrême est l'idée que vous avez préconisée et que chaque type d'exception levée peut être déclaré. Malheureusement, les gens semblent s'opposer à la conséquence de cela, c'est que vous avez un grand nombre de blocs catch afin que vous puissiez gérer chaque type d'exception. Ainsi, par exemple, en Java, ils lanceront Exception réduisant la situation à la même chose que dans Swift ou pire, ils utilisent des exceptions non contrôlées pour que vous puissiez ignorer le problème La bibliothèque GSON est un exemple de cette dernière approche.

Nous avons choisi d'utiliser des exceptions non vérifiées pour indiquer un échec d'analyse. Cela est principalement dû au fait que le client ne peut généralement pas récupérer d'une entrée incorrecte, et donc les forcer à intercepter une exception vérifiée entraîne un code bâclé dans le bloc catch ().

https://github.com/google/gson/blob/master/GsonDesignDocument.md

C'est une très mauvaise décision. "Salut, on ne peut pas vous faire confiance pour faire votre propre gestion des erreurs, donc votre application devrait se bloquer à la place".

Personnellement, je pense que Swift obtient l'équilibre à peu près correct. Vous devez gérer les erreurs, mais vous n'avez pas à écrire des tonnes d'instructions catch pour le faire. S'ils allaient plus loin, les gens trouver des moyens de renverser le mécanisme.

La justification complète de la décision de conception se trouve à https://github.com/Apple/Swift/blob/master/docs/ErrorHandlingRationale.rst

[~ # ~] modifier [~ # ~]

Il semble que certaines personnes aient des problèmes avec certaines des choses que j'ai dites. Voici donc une explication.

Il existe deux grandes catégories de raisons pour lesquelles un programme peut lever une exception.

  • des conditions inattendues dans l'environnement externe au programme, telles qu'une erreur IO sur un fichier ou des données mal formées. Ce sont des erreurs que l'application peut généralement traiter, par exemple en signalant l'erreur à l'utilisateur et leur permettant de choisir un plan d'action différent.
  • Erreurs de programmation telles que pointeur nul ou erreurs liées au tableau. La bonne façon de résoudre ces problèmes consiste pour le programmeur à modifier le code.

Le deuxième type d'erreur ne doit pas, en général, être détecté, car il indique une fausse hypothèse sur l'environnement qui pourrait signifier que les données du programme sont corrompues. Il n'y a aucun moyen de continuer en toute sécurité, vous devez donc avorter.

Le premier type d'erreur peut généralement être récupéré, mais pour récupérer en toute sécurité, chaque trame de pile doit être déroulée correctement, ce qui signifie que la fonction correspondant à chaque trame de pile doit être consciente que les fonctions qu'elle appelle peuvent lever une exception et prendre des mesures pour s'assurer que tout est nettoyé de manière cohérente si une exception est levée, avec, par exemple, un bloc finally ou équivalent. Si le compilateur ne prend pas en charge le fait de dire au programmeur qu'il a oublié de planifier des exceptions, le programmeur ne planifiera pas toujours les exceptions et écrira du code qui perd des ressources ou laisse des données dans un état incohérent.

La raison pour laquelle l'attitude gson est si épouvantable, c'est parce qu'ils disent que vous ne pouvez pas récupérer d'une erreur d'analyse (en fait, pire, ils vous disent que vous n'avez pas les compétences nécessaires pour récupérer d'une erreur d'analyse). C'est une chose ridicule à affirmer, les gens tentent d'analyser les fichiers JSON invalides tout le temps. Est-ce une bonne chose que mon programme se bloque si quelqu'un sélectionne un fichier XML par erreur? Non, non. Il devrait signaler le problème et leur demander de sélectionner un fichier différent.

Et la chose gson n'était, en passant, qu'un exemple de la raison pour laquelle l'utilisation d'exceptions non vérifiées pour les erreurs dont vous pouvez récupérer est mauvaise. Si je veux récupérer après que quelqu'un a sélectionné un fichier XML, je dois intercepter Java exceptions d'exécution, mais lesquelles? Eh bien, je pourrais le vérifier dans les documents Gson, en supposant qu'ils sont corrects) et s'ils étaient allés avec des exceptions vérifiées, l'API me dirait à quelles exceptions s'attendre et le compilateur me dirait si je ne les gère pas.

23
JeremyP

J'ai été l'un des premiers partisans des erreurs typées dans Swift. C'est ainsi que l'équipe Swift m'a convaincu que j'avais tort.

Les erreurs fortement typées sont fragiles de manière à conduire à une mauvaise évolution de l'API. Si l'API promet de ne déclencher qu'une seule des 3 erreurs précises, alors lorsqu'une quatrième condition d'erreur se produit dans une version ultérieure, j'ai le choix: je l'enterre d'une manière ou d'une autre dans la 3 existante, ou je force chaque appelant à réécrire son code de gestion des erreurs pour y faire face. Comme ce n'était pas dans l'original 3, ce n'est probablement pas une condition très courante, et cela met une forte pression sur les API pour ne pas étendre leur liste d'erreurs, en particulier lorsqu'une infrastructure est largement utilisée sur une longue période (pensez: Foundation ).

Bien sûr, avec des énumérations ouvertes, nous pouvons éviter cela, mais une énumération ouverte n'atteint aucun des objectifs d'une erreur fortement typée. Il s'agit essentiellement d'une erreur non typée à nouveau car vous avez toujours besoin d'une "valeur par défaut".

Vous pouvez toujours dire "au moins, je sais d'où vient l'erreur avec une énumération ouverte", mais cela a tendance à aggraver les choses. Disons que j'ai un système de journalisation et qu'il essaie d'écrire et obtient une erreur IO. Que doit-il retourner? Swift n'a pas de types de données algébriques (I ne peut pas dire () -> IOError | LoggingError), donc je devrais probablement envelopper IOError dans LoggingError.IO(IOError) (ce qui oblige chaque couche à reconditionner explicitement; vous ne pouvez pas avoir rethrows très souvent). Même s'il y avait des ADT, voulez-vous vraiment IOError | MemoryError | LoggingError | UnexpectedError | ...? Une fois que vous avez quelques couches, je me retrouve avec couche après couche une enveloppe de "cause racine" sous-jacente qui doivent être péniblement déballés pour faire face.

Et comment allez-vous y faire face? Dans l'écrasante majorité des cas, à quoi ressemblent les blocs de capture?

} catch {
    logError(error)
    return
}

Il est extrêmement rare que les programmes Cocoa (c'est-à-dire les "applications") approfondissent la cause exacte de l'erreur et effectuent différentes opérations en fonction de chaque cas précis. Il peut y avoir un ou deux cas qui ont une récupération, et les autres sont des choses sur lesquelles vous ne pouvez rien faire de toute façon. (Il s'agit d'un problème courant dans Java avec une exception vérifiée qui n'est pas seulement Exception; ce n'est pas comme si personne n'avait emprunté ce chemin auparavant. J'aime Les arguments de Yegor Bugayenko pour les exceptions vérifiées en Java qui fait essentiellement valoir comme son préféré Java pratique exactement la Swift solution.)

Cela ne veut pas dire qu'il n'y a pas de cas où des erreurs fortement typées seraient extrêmement utiles. Mais il y a deux réponses à cela: d'abord, vous êtes libre d'implémenter vous-même des erreurs fortement typées avec une énumération et d'obtenir une assez bonne application du compilateur. Pas parfait (vous avez toujours besoin d'une capture par défaut à l'extérieur de l'instruction switch, mais pas à l'intérieur ), mais plutôt bien si vous suivez vous-même certaines conventions.

Deuxièmement, si ce cas d'utilisation s'avère important (et il pourrait l'être), il n'est pas difficile d'ajouter plus tard des erreurs fortement typées pour ces cas sans casser les cas courants qui souhaitent une gestion des erreurs assez générique. Ils ajouteraient simplement la syntaxe:

func something() throws MyError { }

Et les appelants devraient traiter cela comme un type fort.

Enfin, pour que les erreurs fortement typées soient d'une grande utilité, Foundation devrait les jeter car c'est le plus grand producteur d'erreurs du système. (À quelle fréquence créez-vous vraiment un NSError à partir de zéro par rapport à celui généré par Foundation?) Ce serait une refonte massive de Foundation et très difficile à maintenir compatible avec le code existant et ObjC. Les erreurs typées devraient donc être absolument fantastiques pour résoudre des problèmes Cocoa très courants pour être considérées comme le comportement par défaut. Cela ne pourrait pas être juste un peu plus agréable (sans parler des problèmes décrits ci-dessus).

Rien de tout cela ne veut donc dire que les erreurs non typées sont la solution parfaite à 100% à la gestion des erreurs dans tous les cas. Mais ces arguments m'ont convaincu que c'était la bonne façon d'aller dans Swift aujourd'hui.

31
Rob Napier