Vorwort
Dieser Artikel ist ein Beispiel dafür, wie wir das Verhalten von Swift Standard Library-Funktionen untersuchen können, indem wir unser Wissen nicht nur auf der Bibliotheksdokumentation, sondern auch auf dem Quellcode aufbauen.
Nicht behebbare Fehler
Alle Ereignisse, die Programmierer als "Fehler" bezeichnen, können in zwei Typen unterteilt werden.
- Ereignisse, die durch externe Faktoren wie einen Netzwerkverbindungsfehler verursacht werden.
- Ereignisse, die durch einen Fehler eines Programmierers verursacht wurden, z. B. das Erreichen eines Switch-Operator-Falls, der nicht erreichbar sein sollte.
Die Ereignisse des ersten Typs werden in einem regulären Kontrollfluss verarbeitet. Zum Beispiel reagieren wir auf Netzwerkfehler, indem wir einem Benutzer eine Nachricht anzeigen und eine App so einstellen, dass sie auf die Wiederherstellung der Netzwerkverbindung wartet.
Wir versuchen, Ereignisse des zweiten Typs so früh wie möglich herauszufinden und zu beseitigen, bevor der Code in Produktion geht. Einer der Ansätze hier besteht darin, einige Laufzeitprüfungen durchzuführen , die die Programmausführung in einem debuggbaren Zustand beenden, und eine Nachricht mit einer Angabe zu drucken, wo im Code der Fehler aufgetreten ist.
Beispielsweise kann ein Programmierer die Ausführung beenden, wenn der erforderliche Initialisierer nicht bereitgestellt, sondern aufgerufen wurde. Dies wird beim ersten Testlauf immer bemerkt und behoben.
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
Ein weiteres Beispiel ist das Umschalten zwischen Indizes (nehmen wir an, dass Sie aus irgendeinem Grund keine Aufzählung verwenden können).
switch index { case 0:
Auch hier wird ein Programmierer beim Debuggen einen Absturz verursachen, um unweigerlich einen Fehler bei der Indizierung zu bemerken.
Es gibt fünf Abschlussfunktionen aus der Swift Standard Library (wie bei 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
Welche der fünf Abschlussfunktionen sollten wir bevorzugen?
Quellcode vs Dokumentation
Schauen wir uns den Quellcode an . Folgendes können wir sofort sehen:
- Jede dieser fünf Funktionen beendet entweder die Programmausführung oder führt nichts aus.
- Eine mögliche Kündigung erfolgt auf zwei Arten.
- Beim Drucken einer praktischen Debug-Nachricht durch Aufrufen von
_assertionFailure(_:_:file:line:flags:)
. - Ohne die Debug-Nachricht nur durch Aufrufen von
Builtin.condfail(error._value)
oder Builtin.int_trap()
.
- Der Unterschied zwischen den fünf Abschlussfunktionen liegt in den Bedingungen, unter denen all dies geschieht.
fatalError(_:file:line)
ruft _assertionFailure(_:_:file:line:flags:)
.- Die anderen vier Abschlussfunktionen bewerten die Bedingungen durch Aufrufen der folgenden Konfigurationsbewertungsfunktionen. (Sie beginnen mit einem Unterstrich, was bedeutet, dass sie intern sind und nicht direkt von einem Programmierer aufgerufen werden sollen, der die Swift Standard Library verwendet.)
_isReleaseAssertConfiguration()
_isDebugAssertConfiguration()
_isFastAssertConfiguration()
Schauen wir uns nun die Dokumentation an . Wir können das Folgende sofort sehen.
fatalError(_:file:line)
druckt bedingungslos eine bestimmte Nachricht und stoppt die Ausführung .- Die Auswirkungen der anderen vier Beendigungsfunktionen variieren je nach verwendetem Build-Flag:
-Onone
, -O
, -Ounchecked
. Schauen Sie sich zum Beispiel die Dokumentation zu preconditionFailure(_:file:line:)
. - Wir können diese Build-Flags in Xcode über
SWIFT_OPTIMIZATION_LEVEL
Compiler-Build-Einstellung SWIFT_OPTIMIZATION_LEVEL
setzen. - Aus der Xcode 10- Dokumentation wissen wir auch, dass ein weiteres Optimierungsflag -
-Osize
- eingeführt wird. - Daher müssen wir die vier Optimierungs-Build-Flags berücksichtigen.
-Onone
(nicht optimieren)-O
(Geschwindigkeit optimieren)-Osize
(Größe optimieren)-Ounchecked
(viele Compilerprüfungen ausschalten)
Wir können daraus schließen, dass die in den vier Abschlussfunktionen ausgewertete Konfiguration durch diese Build-Flags festgelegt wird.
Ausführen von Konfigurationsbewertungsfunktionen
Obwohl Konfigurationsbewertungsfunktionen für den internen Gebrauch konzipiert sind, sind einige von ihnen zu Testzwecken öffentlich , und wir können sie über die CLI testen und die folgenden Befehle in Bash eingeben.
$ 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
Diese Tests und die Überprüfung des Quellcodes führen uns zu den folgenden groben Schlussfolgerungen.
Es gibt drei sich gegenseitig ausschließende Konfigurationen.
- Die Release- Konfiguration wird festgelegt, indem entweder ein
-Osize
oder ein -Osize
Build-Flag -Osize
. - Die Debug- Konfiguration wird festgelegt, indem entweder ein
-Onone
Build-Flag oder gar keine Optimierungsflags -Onone
. _isFastAssertConfiguration()
wird als true
ausgewertet, wenn ein Build-Flag -Ounchecked
gesetzt ist. Obwohl diese Funktion ein Wort "schnell" im Namen hat, hat sie nichts mit der Optimierung für das Geschwindigkeit -O
Build-Flag zu tun.
NB: Diese Schlussfolgerungen sind nicht die strikte Definition, wann Debug- oder Release- Builds stattfinden. Es ist ein komplexeres Thema. Diese Schlussfolgerungen sind jedoch für den Kontext der Beendigung der Funktionsnutzung richtig.
Das Bild vereinfachen
-Ounchecked
Schauen wir uns nicht an, wofür das Flag -Ounchecked
ist (hier ist es irrelevant), sondern welche Rolle es im Zusammenhang mit der Beendigung der Funktionsnutzung spielt.
- In der Dokumentation für die
precondition(_:_:file:line:)
und assert(_:_:file:line:)
heißt es: "In -Ounchecked
Builds wird die Bedingung nicht ausgewertet, aber das Optimierungsprogramm kann davon ausgehen, dass sie immer als true ausgewertet wird. Die Nichterfüllung dieser Annahme ist ein schwerwiegender Programmierfehler. " - In der Dokumentation für
preconditionFailure(_:file:line)
und assertionFailure(_:file:line:)
heißt es: "In -Ounchecked
Builds kann der Optimierer davon ausgehen, dass diese Funktion niemals aufgerufen wird. Die -Ounchecked
dieser Annahme ist ein schwerwiegender Programmierfehler. "" - Wir können dem Quellcode
_isFastAssertConfiguration()
dass die Auswertung von _isFastAssertConfiguration()
zu true
nicht erfolgen sollte . (In diesem Fall wird das seltsame _conditionallyUnreachable()
aufgerufen. Siehe Zeilen 136 und _conditionallyUnreachable()
)
Wenn Sie direkter sprechen, dürfen Sie nicht zulassen, dass die folgenden vier Beendigungsfunktionen mit dem für Ihr Programm gesetzten Flag -Ounchecked
Build erreichbar sind.
precondition(_:_:file:line:)
preconditionFailure(_:file:line)
assert(_:_:file:line:)
assertionFailure(_:file:line:)
Verwenden fatalError(_:file:line)
beim Anwenden von -Ounchecked
nur fatalError(_:file:line)
und lassen Sie gleichzeitig zu, dass der Punkt Ihres Programms mit der fatalError(_:file:line)
erreichbar ist.
Die Rolle einer Zustandsprüfung
Mit zwei der Abschlussfunktionen können wir die Bedingungen überprüfen. Durch die Überprüfung des Quellcodes können wir feststellen, dass das Funktionsverhalten bei einem fehlgeschlagenen Zustand dem Verhalten des jeweiligen Cousins entspricht:
precondition(_:_:file:line:)
wird preconditionFailure(_:file:line)
,assert(_:_:file:line:)
wird zu assertionFailure(_:file:line:)
.
Dieses Wissen erleichtert die weitere Analyse.
Release vs Debug-Konfigurationen
Durch weitere Dokumentation und Überprüfung des Quellcodes können wir schließlich die folgende Tabelle formulieren.

Es ist jetzt klar, dass die wichtigste Wahl für einen Programmierer ist, wie das Programmverhalten in der Version aussehen sollte, wenn eine Laufzeitprüfung einen Fehler aufdeckt.
Der Schlüssel zum Erfolg ist, dass assert(_:_:file:line:)
und assertionFailure(_:file:line:)
die Auswirkungen von Programmfehlern weniger schwerwiegend machen. Beispielsweise hat eine iOS-App möglicherweise die Benutzeroberfläche beschädigt (da einige wichtige Laufzeitprüfungen fehlgeschlagen sind), stürzt jedoch nicht ab.
Aber dieses Szenario ist möglicherweise nicht das, das Sie wollten. Sie haben die Wahl.
Never
Typ zurück
Never
wird als Rückgabefunktion verwendet, die bedingungslos einen Fehler auslöst, abfängt oder auf andere Weise nicht normal beendet wird. Diese Arten von Funktionen kehren nicht zurück, sie kehren niemals zurück.
Unter den fünf Abschlussfunktionen geben nur preconditionFailure(_:file:line)
und fatalError(_:file:line)
Never
da nur diese beiden Funktionen die Programmausführung bedingungslos stoppen und daher niemals zurückkehren.
Hier ist ein schönes Beispiel für die Verwendung von Never
type in einer Befehlszeilen-App. (Obwohl in diesem Beispiel keine Abschlussfunktionen der Swift Standard Library verwendet werden, sondern stattdessen die Standardfunktion C exit()
).
func printUsagePromptAndExit() -> Never { print("Usage: command directory") exit(1) } guard CommandLine.argc == 2 else { printUsagePromptAndExit() }
Wenn printUsagePromptAndExit()
Void
anstelle von Never
zurückgibt, wird ein Buildtime-Fehler mit der Meldung " 'guard' body darf nicht durchfallen, verwenden Sie 'return' oder 'throw', um den Bereich zu verlassen " erhalten. Wenn Sie Never
, sagen Sie im Voraus, dass Sie den Bereich niemals verlassen und der Compiler Ihnen daher keinen Buildtime-Fehler gibt. Andernfalls sollten Sie am Ende des Guard-Code-Blocks return
hinzufügen, was nicht gut aussieht.
Imbissbuden
- Es spielt keine Rolle, welche Abschlussfunktion verwendet werden soll, wenn Sie sicher sind, dass alle Ihre Laufzeitprüfungen nur für die Debug- Konfiguration relevant sind.
- Verwenden
fatalError(_:file:line)
beim Anwenden von -Ounchecked
nur fatalError(_:file:line)
und lassen Sie gleichzeitig zu, dass der Punkt Ihres Programms mit der fatalError(_:file:line)
erreichbar ist. - Verwenden Sie
assert(_:_:file:line:)
und assertionFailure(_:file:line:)
wenn Sie befürchten, dass Laufzeitprüfungen in der Version fehlschlagen könnten. Zumindest stürzt Ihre App nicht ab. - Verwenden Sie
Never
, um Ihren Code ordentlich aussehen zu lassen.
Nützliche Links