Die Verarbeitung nicht behebbarer Fehler in Swift

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: // something is done here case 1: // other thing is done here case 2: // and other thing is done here default: assertionFailure("Impossible index") } 

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:


  1. Jede dieser fünf Funktionen beendet entweder die Programmausführung oder führt nichts aus.
  2. 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() .
  3. Der Unterschied zwischen den fünf Abschlussfunktionen liegt in den Bedingungen, unter denen all dies geschieht.
  4. fatalError(_:file:line) ruft _assertionFailure(_:_:file:line:flags:) .
  5. 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.


  1. fatalError(_:file:line) druckt bedingungslos eine bestimmte Nachricht und stoppt die Ausführung .
  2. 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:) .
  3. Wir können diese Build-Flags in Xcode über SWIFT_OPTIMIZATION_LEVEL Compiler-Build-Einstellung SWIFT_OPTIMIZATION_LEVEL setzen.
  4. Aus der Xcode 10- Dokumentation wissen wir auch, dass ein weiteres Optimierungsflag - -Osize - eingeführt wird.
  5. 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.


Funktionen beenden


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.


Source: https://habr.com/ru/post/de440884/


All Articles