Préface
Cet article est un exemple de la façon dont nous pouvons effectuer des recherches sur le comportement des fonctions de la bibliothèque standard Swift en renforçant nos connaissances non seulement sur la documentation de la bibliothèque, mais également sur son code source.
Erreurs irrécupérables
Tous les événements que les programmeurs appellent des "erreurs" peuvent être séparés en deux types.
- Événements causés par des facteurs externes tels qu'une panne de connexion réseau.
- Événements causés par l'erreur d'un programmeur, comme atteindre un boîtier d'opérateur de commutateur qui devrait être inaccessible.
Les événements du premier type sont traités dans un flux de contrôle régulier. Par exemple, nous réagissons à une défaillance du réseau en affichant un message à un utilisateur et en configurant une application pour attendre la récupération de la connexion réseau.
Nous essayons de découvrir et d'éliminer les événements du deuxième type le plus tôt possible avant la mise en production du code. Une des approches ici consiste à exécuter des vérifications d'exécution mettant fin à l'exécution du programme dans un état débogable et à imprimer un message avec une indication de l'endroit où le code s'est produit.
Par exemple, un programmeur peut mettre fin à l'exécution si l'initialiseur requis n'a pas été fourni mais a été appelé. Cela sera invariablement remarqué et corrigé lors du premier test.
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
Un autre exemple est la commutation entre les indices (supposons que pour une raison quelconque, vous ne pouvez pas utiliser l'énumération).
switch index { case 0:
Encore une fois, un programmeur va provoquer un crash lors du débogage ici afin de remarquer inévitablement un bogue dans l'indexation.
Il existe cinq fonctions de terminaison de la bibliothèque standard Swift (comme pour Swift 4.2).
func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
func preconditionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never
func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
func assertionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
func fatalError(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never
Laquelle des cinq fonctions terminales devrions-nous préférer?
Code source vs documentation
Regardons le code source . On voit tout de suite ce qui suit:
- Chacune de ces cinq fonctions met fin à l'exécution du programme ou ne fait rien.
- La résiliation possible se produit de deux manières.
- Avec l'impression d'un message de débogage pratique en appelant
_assertionFailure(_:_:file:line:flags:)
. - Sans le message de débogage simplement en appelant
Builtin.condfail(error._value)
ou Builtin.int_trap()
.
- La différence entre les cinq fonctions terminales réside dans les conditions dans lesquelles tout ce qui précède se produit.
fatalError(_:file:line)
appelle _assertionFailure(_:_:file:line:flags:)
sans condition.- Les quatre autres fonctions terminales évaluent les conditions en appelant les fonctions d'évaluation de configuration suivantes. (Ils commencent par un trait de soulignement, ce qui signifie qu'ils sont internes et ne sont pas censés être appelés directement par un programmeur qui utilise la bibliothèque standard Swift).
_isReleaseAssertConfiguration()
_isDebugAssertConfiguration()
_isFastAssertConfiguration()
Examinons maintenant la documentation . Nous pouvons voir tout de suite ce qui suit.
fatalError(_:file:line)
imprime inconditionnellement un message donné et arrête l'exécution .- Les effets des quatre autres fonctions de terminaison varient en fonction de l'indicateur de génération utilisé:
-Onone
, -O
, -Ounchecked
. Par exemple, consultez la documentation de preconditionFailure(_:file:line:)
. - Nous pouvons définir ces indicateurs de génération dans Xcode via le
SWIFT_OPTIMIZATION_LEVEL
compilateur SWIFT_OPTIMIZATION_LEVEL
. - Nous savons également à partir de la documentation Xcode 10 qu'un autre indicateur d'optimisation -
-Osize
- a été introduit. - Nous avons donc les quatre indicateurs de construction d'optimisation à considérer.
-Onone
(ne pas optimiser)-O
(optimiser pour la vitesse)-Osize
(optimiser la taille)-Ounchecked
(désactiver de nombreuses vérifications du compilateur)
Nous pouvons conclure que la configuration évaluée dans les quatre fonctions terminales est définie par ces drapeaux de construction.
Exécution des fonctions d'évaluation de la configuration
Bien que les fonctions d'évaluation de la configuration soient conçues pour un usage interne, certaines d'entre elles sont publiques à des fins de test , et nous pouvons les essayer via CLI en donnant les commandes suivantes dans Bash.
$ echo 'print(_isFastAssertConfiguration())' >conf.swift $ swift conf.swift false $ swift -Onone conf.swift false $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift true
$ echo 'print(_isDebugAssertConfiguration())' >conf.swift $ swift conf.swift true $ swift -Onone conf.swift true $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift false
Ces tests et l'inspection du code source nous conduisent aux conclusions approximatives suivantes.
Il existe trois configurations mutuellement exclusives.
- La configuration des
-Osize
est définie en fournissant un -Osize
build -O
ou -Osize
. - La configuration de débogage est définie en fournissant soit un
-Onone
build -Onone
, soit aucun indicateur d'optimisation. _isFastAssertConfiguration()
est évalué à true
si un -Ounchecked
build -Ounchecked
est défini. Bien que cette fonction ait un mot «rapide» dans son nom, elle n'a rien à voir avec l'optimisation de l'indicateur de build speed -O
.
NB: Ces conclusions ne sont pas la définition stricte du moment où les versions de débogage ou de version ont lieu. C'est une question plus complexe. Mais ces conclusions sont correctes pour le contexte d'utilisation des fonctions de terminaison.
Simplifier l'image
-Ounchecked
Examinons non pas à quoi -Ounchecked
indicateur -Ounchecked
(il n'est pas pertinent ici), mais quel est son rôle dans le contexte de la fin de l'utilisation des fonctions.
- La documentation de la
precondition(_:_:file:line:)
et de l' assert(_:_:file:line:)
indique: "Dans les versions -Ounchecked
, la condition n'est pas évaluée, mais l'optimiseur peut supposer qu'elle a toujours la valeur true. Le non-respect de cette hypothèse est une grave erreur de programmation. " - La documentation de
preconditionFailure(_:file:line)
et assertionFailure(_:file:line:)
indique: "Dans les versions -Ounchecked
, l'optimiseur peut supposer que cette fonction n'est jamais appelée. Le non-respect de cette hypothèse est une grave erreur de programmation. " - Nous pouvons voir dans le code source que l'évaluation de
_isFastAssertConfiguration()
sur true
ne devrait pas se produire . (Si cela se produit, étrange _conditionallyUnreachable()
est appelé. Voir voir lignes 136 et _conditionallyUnreachable()
)
En parlant plus directement, vous ne devez pas autoriser l'accessibilité des quatre fonctions de terminaison suivantes avec l' -Ounchecked
construction -Ounchecked
défini pour votre programme.
precondition(_:_:file:line:)
preconditionFailure(_:file:line)
assert(_:_:file:line:)
assertionFailure(_:file:line:)
Utilisez uniquement fatalError(_:file:line)
tout en appliquant -Ounchecked
et en même temps, ce qui permet que le point de votre programme avec l' fatalError(_:file:line)
soit accessible.
Le rôle d'un contrôle d'état
Deux des fonctions de terminaison nous permettent de vérifier les conditions. L'inspection du code source nous permet de voir que si la condition échoue alors le comportement de la fonction est le même que le comportement de son cousin respectif:
precondition(_:_:file:line:)
devient preconditionFailure(_:file:line)
,assert(_:_:file:line:)
devient assertionFailure(_:file:line:)
.
Cette connaissance facilite l'analyse approfondie.
Versions vs configurations de débogage
Finalement, une documentation supplémentaire et une inspection du code source nous permettent de formuler le tableau suivant.

Il est clair maintenant que le choix le plus important pour un programmeur est le comportement du programme dans la version si une vérification d'exécution révèle une erreur.
La clé à retenir ici est que assert(_:_:file:line:)
et assertionFailure(_:file:line:)
rendent l'impact de l'échec du programme moins grave. Par exemple, une application iOS peut avoir corrompu l'interface utilisateur (car certaines vérifications importantes de l'exécution ont échoué), mais elle ne se bloquera pas.
Mais ce scénario n'est peut-être pas celui que vous vouliez. Vous avez le choix.
Never
renvoyer le type
Never
est utilisé comme type de fonction de retour qui génère sans condition une erreur, intercepte ou ne se termine pas normalement. Ces types de fonctions ne reviennent pas réellement, ils ne reviennent jamais .
Parmi les cinq fonctions terminales, seules preconditionFailure(_:file:line)
et fatalError(_:file:line)
renvoient Never
car seules ces deux fonctions arrêtent inconditionnellement les exécutions de programme et ne reviennent donc jamais.
Voici un bel exemple d'utilisation de Never
type dans une application en ligne de commande. (Bien que cet exemple n'utilise pas les fonctions de terminaison de la bibliothèque standard Swift mais la fonction C exit()
standard à la place).
func printUsagePromptAndExit() -> Never { print("Usage: command directory") exit(1) } guard CommandLine.argc == 2 else { printUsagePromptAndExit() }
Si printUsagePromptAndExit()
renvoie Void
au lieu de Never
, vous obtenez une erreur de génération avec le message " le corps de 'guard' ne doit pas tomber, envisagez d'utiliser un 'return' ou 'throw' pour quitter la portée ". En utilisant Never
vous dites à l'avance que vous ne quittez jamais la portée et que le compilateur ne vous donnera donc pas d'erreur de compilation. Sinon, vous devez ajouter un return
à la fin du bloc de code de garde, ce qui n'est pas joli.
Plats à emporter
- Peu importe la fonction de terminaison à utiliser si vous êtes sûr que toutes vos vérifications d'exécution ne sont pertinentes que pour la configuration de débogage .
- Utilisez uniquement
fatalError(_:file:line)
tout en appliquant -Ounchecked
et en même temps, ce qui permet que le point de votre programme avec l' fatalError(_:file:line)
soit accessible. - Utilisez
assert(_:_:file:line:)
et assertionFailure(_:file:line:)
si vous craignez que les vérifications d'exécution puissent échouer d'une manière ou d'une autre dans la version. Au moins, votre application ne se bloquera pas. - Utilisez
Never
pour que votre code soit net.
Liens utiles