Schnelle Do-Try-Catch-Syntax

162

Ich versuche es in Swift 2 mit neuen Fehlern zu verstehen. Folgendes habe ich getan: Ich habe zuerst eine Fehleraufzählung deklariert:

enum SandwichError: ErrorType {
    case NotMe
    case DoItYourself
}

Und dann habe ich eine Methode deklariert, die einen Fehler auslöst (keine Ausnahme, Leute. Es ist ein Fehler.). Hier ist diese Methode:

func makeMeSandwich(names: [String: String]) throws -> String {
    guard let sandwich = names["sandwich"] else {
        throw SandwichError.NotMe
    }

    return sandwich
}

Das Problem ist von der anrufenden Seite. Hier ist der Code, der diese Methode aufruft:

let kitchen = ["sandwich": "ready", "breakfeast": "not ready"]

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
    print("Not me error")
} catch SandwichError.DoItYourself {
    print("do it error")
}

Nachdem der doZeilencompiler sagt Errors thrown from here are not handled because the enclosing catch is not exhaustive. Aber meiner Meinung nach ist es erschöpfend, weil es nur zwei Fälle in der SandwichErrorAufzählung gibt.

Für reguläre switch-Anweisungen kann swift verstehen, dass es erschöpfend ist, wenn jeder Fall behandelt wird.

mustafa
quelle
3
Sie geben nicht die Art des Fehlers an, den Sie
auslösen
Gibt es eine Möglichkeit, die Art des Fehlers anzugeben?
Mustafa
Ich kann nichts in der neuen Version des Swift-Buches finden - nur das Wurf-Schlüsselwort im
Moment
Funktioniert für mich auf einem Spielplatz ohne Fehler oder Warnungen.
Fogmeister
2
Spielplätze scheinen doBlöcke auf der obersten Ebene zuzulassen , die nicht erschöpfend sind. Wenn Sie das Do in eine nicht werfende Funktion einwickeln, wird der Fehler generiert.
Sam

Antworten:

267

Das Swift 2-Fehlerbehandlungsmodell weist zwei wichtige Punkte auf: Vollständigkeit und Ausfallsicherheit. Zusammen laufen sie auf Ihre do/ catch-Anweisung hinaus und müssen jeden möglichen Fehler abfangen, nicht nur die, von denen Sie wissen, dass Sie sie werfen können.

Beachten Sie, dass Sie nicht deklarieren, welche Arten von Fehlern eine Funktion auslösen kann, sondern nur, ob sie überhaupt auslöst. Es ist ein Null-Eins-Unendlich-Problem: Als jemand, der eine Funktion definiert, die von anderen (einschließlich Ihres zukünftigen Selbst) verwendet werden soll, möchten Sie nicht jeden Client Ihrer Funktion dazu bringen müssen, sich an jede Änderung in der Implementierung Ihrer Funktion anzupassen Funktion, einschließlich der Fehler, die es auslösen kann. Sie möchten, dass Code, der Ihre Funktion aufruft, solchen Änderungen standhält.

Da Ihre Funktion nicht sagen kann, welche Art von Fehlern sie auslöst (oder in Zukunft auslösen könnte), catchwissen die Blöcke, die sie abfangen, nicht, welche Arten von Fehlern sie auslösen könnte. Zusätzlich zu den Fehlertypen, die Sie kennen, müssen Sie diejenigen, die Sie nicht kennen, mit einer universellen catchAnweisung behandeln. Wenn Ihre Funktion also die Menge der Fehler ändert, die sie in Zukunft auslöst, werden die Anrufer diese weiterhin abfangen Fehler.

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
    print("Not me error")
} catch SandwichError.DoItYourself {
    print("do it error")
} catch let error {
    print(error.localizedDescription)
}

Aber lasst uns hier nicht aufhören. Denken Sie noch etwas über diese Resilienzidee nach. So wie Sie Ihr Sandwich entworfen haben, müssen Sie Fehler an jedem Ort beschreiben, an dem Sie sie verwenden. Das bedeutet, dass Sie jedes Mal, wenn Sie die Fehlerfälle ändern, jeden Ort ändern müssen, an dem sie verwendet werden ... nicht sehr lustig.

Die Idee hinter der Definition Ihrer eigenen Fehlertypen besteht darin, dass Sie solche Dinge zentralisieren können. Sie können eine descriptionMethode für Ihre Fehler definieren:

extension SandwichError: CustomStringConvertible {
    var description: String {
        switch self {
            case NotMe: return "Not me error"
            case DoItYourself: return "Try sudo"
        }
    }
}

Und dann kann Ihr Fehlerbehandlungscode Ihren Fehlertyp auffordern, sich selbst zu beschreiben - jetzt kann jeder Ort, an dem Sie Fehler behandeln, denselben Code verwenden und auch mögliche zukünftige Fehlerfälle behandeln.

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch let error as SandwichError {
    print(error.description)
} catch {
    print("i dunno")
}

Dies ebnet auch den Weg für Fehlertypen (oder Erweiterungen auf ihnen), um andere Arten der Fehlermeldung zu unterstützen. Sie könnten beispielsweise eine Erweiterung für Ihren Fehlertyp haben, die weiß, wie eine UIAlertControllerFehlermeldung für einen iOS-Benutzer angezeigt wird.

Rickster
quelle
1
@rickster: Könntest du den Compilerfehler wirklich reproduzieren? Der Originalcode wird für mich ohne Fehler oder Warnungen kompiliert. Und wenn eine nicht abgefangene Ausnahme ausgelöst wird, bricht das Programm mit ab. Obwohl error caught in main()alles, was Sie gesagt haben, vernünftig klingt, kann ich dieses Verhalten nicht reproduzieren.
Martin R
5
Ich liebe es, wie Sie Fehlermeldungen in einer Erweiterung getrennt haben. Wirklich schöne Möglichkeit, Ihren Code sauber zu halten! Tolles Beispiel!
Konrad77
Es wird dringend empfohlen, die Verwendung des erzwungenen tryAusdrucks im Produktionscode zu vermeiden, da dies einen Laufzeitfehler verursachen und zum Absturz Ihrer Anwendung führen kann
Otar
@Otar gute Gedanken im Allgemeinen, aber es ist ein wenig abseits des Themas - die Antwort bezieht sich nicht auf die Verwendung (oder nicht Verwendung) try!. Es gibt wohl auch gültige, "sichere" Anwendungsfälle für die verschiedenen "Force" -Operationen in Swift (Auspacken, Ausprobieren usw.), selbst für Produktionscode - wenn Sie durch Vorbedingungen oder Konfiguration die Möglichkeit eines Fehlers zuverlässig ausgeschlossen haben, könnte dies der Fall sein Es ist sinnvoller, einen Kurzschluss auf einen sofortigen Fehler kurzzuschließen, als einen nicht testbaren Fehlerbehandlungscode zu schreiben.
Rickster
Wenn Sie nur die Fehlermeldung anzeigen müssen, ist es SandwichErrorsinnvoll , diese Logik in die Klasse einzufügen. Ich vermute jedoch, dass für die meisten Fehler die Fehlerbehandlungslogik nicht so gekapselt werden kann. Dies liegt daran, dass normalerweise die Kenntnis des Kontextes des Anrufers erforderlich ist (ob ein Fehler wiederhergestellt oder erneut versucht oder ein Fehler im Upstream gemeldet werden soll usw.). Mit anderen Worten, ich vermute, dass das häufigste Muster ohnehin darin bestehen müsste, mit bestimmten Fehlertypen übereinzustimmen.
Max
29

Ich vermute, dass dies noch nicht richtig implementiert wurde. Das Swift Programming Guide scheint definitiv zu implizieren, dass der Compiler erschöpfende Übereinstimmungen "wie eine switch-Anweisung" ableiten kann. Es wird nicht erwähnt, dass ein General benötigt catchwird, um erschöpfend zu sein.

Sie werden auch feststellen, dass sich der Fehler in der tryZeile befindet und nicht am Ende des Blocks. Das heißt, der Compiler kann irgendwann feststellen, welche tryAnweisung im Block nicht behandelte Ausnahmetypen hat.

Die Dokumentation ist allerdings etwas mehrdeutig. Ich habe das Video "Was ist neu in Swift?" Durchgesehen und konnte keine Hinweise finden. Ich werde es weiter versuchen.

Aktualisieren:

Wir sind jetzt bis zur Beta 3 ohne Hinweis auf eine ErrorType-Inferenz. Ich glaube jetzt, wenn dies jemals geplant war (und ich denke immer noch, dass es irgendwann war), hat der dynamische Versand von Protokollerweiterungen es wahrscheinlich beendet.

Beta 4 Update:

Xcode 7b4 hat die Unterstützung für Dokumentkommentare hinzugefügt Throws:, mit denen "dokumentiert werden soll, welche Fehler ausgelöst werden können und warum". Ich denke , das zumindest bietet einigen Mechanismus Fehler API Verbraucher zu kommunizieren. Wer braucht ein Typensystem, wenn Sie Dokumentation haben!

Ein weiteres Update:

Nachdem er für die automatische einige Zeit der Hoffnung , ErrorTypeInferenz und arbeiten heraus , was die Grenzen dieses Modells wäre, habe ich meine Meinung geändert - das ist es, was ich hoffe , Apple - Geräte statt. Im Wesentlichen:

// allow us to do this:
func myFunction() throws -> Int

// or this:
func myFunction() throws CustomError -> Int

// but not this:
func myFunction() throws CustomErrorOne, CustomErrorTwo -> Int

Noch ein Update

Die Gründe für die Fehlerbehandlung von Apple finden Sie jetzt hier . Es gab auch einige interessante Diskussionen auf der Mailingliste von Swift-Evolution . Im Wesentlichen ist John McCall gegen typisierte Fehler, da er glaubt, dass die meisten Bibliotheken ohnehin einen generischen Fehlerfall enthalten werden und dass typisierte Fehler, abgesehen von Boilerplate, wahrscheinlich nicht viel zum Code beitragen (er verwendete den Begriff "aspirational bluff"). Chris Lattner sagte, er sei offen für Tippfehler in Swift 3, wenn dies mit dem Resilienzmodell funktioniert.

Sam
quelle
Danke für die Links. Von John jedoch nicht überzeugt: "Viele Bibliotheken enthalten den Typ" anderer Fehler "" bedeutet nicht, dass jeder einen Typ "anderer Fehler" benötigt.
Franklin Yu
Der offensichtliche Zähler ist, dass es keinen einfachen Weg gibt, um zu wissen, welche Art von Fehler eine Funktion auslöst, bis der Entwickler gezwungen ist, alle Fehler abzufangen und zu versuchen, sie so gut wie möglich zu behandeln. Es ist ziemlich nervig, ganz offen zu sein.
William T Froggard
4

Swift befürchtet, dass Ihre case-Anweisung nicht alle Fälle abdeckt. Um dies zu beheben, müssen Sie einen Standardfall erstellen:

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
    print("Not me error")
} catch SandwichError.DoItYourself {
    print("do it error")
} catch Default {
    print("Another Error")
}
Icaro
quelle
2
Aber ist das nicht umständlich? Ich habe nur zwei Fälle und alle in catchAussagen aufgeführt.
Mustafa
2
Jetzt ist ein guter Zeitpunkt für eine Verbesserungsanforderung, die hinzufügt func method() throws(YourErrorEnum), oder throws(YourEnum.Error1, .Error2, .Error3)trotzdem wissen Sie, was geworfen werden kann
Matthias Bauch
8
Das Swift-Compilerteam bei einer der WWDC-Sitzungen machte deutlich, dass es keine pedantischen Listen aller möglichen Fehler "wie Java" wollte.
Sam
4
Es gibt keinen Standard- / Standardfehler.
Lass
1
@Icaro Das macht mich nicht sicher; Wenn ich in Zukunft "einen neuen Eintrag im Array hinzufügen" möchte, sollte der Compiler mich anschreien, weil ich nicht alle betroffenen catch-Klauseln aktualisiert habe.
Franklin Yu
3

Ich war auch enttäuscht über den Mangel an Typ, den eine Funktion auslösen kann, aber ich bekomme ihn jetzt dank @rickster und fasse ihn folgendermaßen zusammen: Nehmen wir an, wir könnten den Typ angeben, den eine Funktion auslöst, wir hätten so etwas:

enum MyError: ErrorType { case ErrorA, ErrorB }

func myFunctionThatThrows() throws MyError { ...throw .ErrorA...throw .ErrorB... }

do {
    try myFunctionThatThrows()
}
case .ErrorA { ... }
case .ErrorB { ... }

Das Problem ist, dass selbst wenn wir in myFunctionThatThrows nichts ändern, wenn wir MyError nur einen Fehlerfall hinzufügen:

enum MyError: ErrorType { case ErrorA, ErrorB, ErrorC }

Wir sind geschraubt, weil unser Do / Try / Catch nicht mehr vollständig ist, ebenso wie jeder andere Ort, an dem wir Funktionen aufgerufen haben, die MyError auslösen

greg3z
quelle
3
Ich bin mir nicht sicher, warum du geschraubt bist. Sie würden einen Compilerfehler bekommen, was ist das, was Sie wollen, richtig? Das passiert mit switch-Anweisungen, wenn Sie einen Enum-Fall hinzufügen.
Sam
In gewissem Sinne schien es mir am wahrscheinlichsten, dass dies bei Fehleraufzählungen / do-Fällen passieren würde, aber es ist genau so, wie es bei Aufzählungen / Schaltern passieren würde, Sie haben Recht. Ich versuche immer noch, mich davon zu überzeugen, dass Apples Entscheidung, nicht zu tippen, was wir werfen, die gute ist, aber Sie helfen mir in dieser Sache nicht! ^^
greg3z
Das manuelle Eingeben von Fehlern würde in nicht trivialen Fällen zu einem großen Durcheinander führen. Die Typen sind eine Vereinigung aller möglichen Fehler aus allen throw- und try-Anweisungen innerhalb der Funktion. Wenn Sie Fehleraufzählungen manuell verwalten, ist dies schmerzhaft. Ein catch {}am Ende jedes Blocks ist jedoch wohl schlechter. Ich hoffe, dass der Compiler irgendwann automatisch auf Fehlertypen schließen wird, wo dies möglich ist, aber ich konnte dies nicht bestätigen.
Sam
Ich bin damit einverstanden, dass der Compiler theoretisch in der Lage sein sollte, auf die Fehlertypen zu schließen, die eine Funktion auslöst. Aber ich denke, es ist auch für den Entwickler sinnvoll, sie aus Gründen der Klarheit explizit aufzuschreiben. In den nicht trivialen Fällen, von denen Sie sprechen, scheint mir die Auflistung der verschiedenen Fehlertypen in Ordnung zu sein: func f () löst ErrorTypeA, ErrorTypeB {}
greg3z
Es fehlt definitiv ein großer Teil darin, dass es keinen Mechanismus zur Übermittlung von Fehlertypen gibt (außer Dokumentkommentaren). Das Swift-Team hat jedoch erklärt, dass es keine expliziten Listen von Fehlertypen möchte. Ich bin sicher, dass die meisten Leute, die sich in der Vergangenheit mit Java-geprüften Ausnahmen befasst haben, zustimmen würden
Sam
1
enum NumberError: Error {
  case NegativeNumber(number: Int)
  case ZeroNumber
  case OddNumber(number: Int)
}

extension NumberError: CustomStringConvertible {
         var description: String {
         switch self {
             case .NegativeNumber(let number):
                 return "Negative number \(number) is Passed."
             case .OddNumber(let number):
                return "Odd number \(number) is Passed."
             case .ZeroNumber:
                return "Zero is Passed."
      }
   }
}

 func validateEvenNumber(_ number: Int) throws ->Int {
     if number == 0 {
        throw NumberError.ZeroNumber
     } else if number < 0 {
        throw NumberError.NegativeNumber(number: number)
     } else if number % 2 == 1 {
         throw NumberError.OddNumber(number: number)
     }
    return number
}

Jetzt Nummer validieren:

 do {
     let number = try validateEvenNumber(0)
     print("Valid Even Number: \(number)")
  } catch let error as NumberError {
     print(error.description)
  }
Yogendra Singh
quelle
-2

Erstellen Sie eine Aufzählung wie folgt:

//Error Handling in swift
enum spendingError : Error{
case minus
case limit
}

Erstellen Sie eine Methode wie:

 func calculateSpending(morningSpending:Double,eveningSpending:Double) throws ->Double{
if morningSpending < 0 || eveningSpending < 0{
    throw spendingError.minus
}
if (morningSpending + eveningSpending) > 100{
    throw spendingError.limit
}
return morningSpending + eveningSpending
}

Überprüfen Sie nun, ob ein Fehler vorliegt oder nicht, und behandeln Sie ihn:

do{
try calculateSpending(morningSpending: 60, eveningSpending: 50)
} catch spendingError.minus{
print("This is not possible...")
} catch spendingError.limit{
print("Limit reached...")
}
Mr.Javed Multani
quelle
Nah aber keine Zigarre. Versuchen Sie, den Abstand zu fixieren und den Kamelkoffer herzustellen.
Alec