Szybka składnia do-try-catch

162

Próbuję zrozumieć nową obsługę błędów w języku Swift 2. Oto co zrobiłem: najpierw zadeklarowałem wyliczenie błędu:

enum SandwichError: ErrorType {
    case NotMe
    case DoItYourself
}

A potem zadeklarowałem metodę, która zgłasza błąd (nie ludzie z wyjątkiem. To jest błąd.). Oto ta metoda:

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

    return sandwich
}

Problem dotyczy strony dzwoniącej. Oto kod, który wywołuje tę metodę:

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")
}

Po tym do, jak kompilator linii mówi Errors thrown from here are not handled because the enclosing catch is not exhaustive. Ale moim zdaniem jest to wyczerpujące, ponieważ w SandwichErrorwyliczeniu są tylko dwa przypadki .

W przypadku zwykłych instrukcji switch, swift może zrozumieć, że jest to wyczerpujące, gdy zajmujemy się każdą sprawą.

mustafa
źródło
3
Nie określasz rodzaju błędu, który
rzucasz
Czy istnieje sposób, aby określić rodzaj błędu?
mustafa
Nie mogę znaleźć niczego w nowej wersji książki Swift - tylko słowo kluczowe rzuca teraz
Farlei Heinen
U mnie działa na placu zabaw bez błędów i ostrzeżeń.
Fogmeister
2
Wydaje się, że place zabaw zezwalają na dobloki na najwyższym poziomie, które nie są wyczerpujące - jeśli zawiniesz do w funkcję nie rzucającą, wygeneruje błąd.
Sam

Odpowiedzi:

267

Istnieją dwa ważne punkty modelu obsługi błędów w Swift 2: wyczerpujący i elastyczny. Razem sprowadzają się do tego, że twoje do/ catchstwierdzenie musi wychwycić każdy możliwy błąd, a nie tylko te, o których wiesz, że możesz je rzucić.

Zwróć uwagę, że nie deklarujesz, jakie typy błędów funkcja może zgłaszać, tylko czy w ogóle zgłasza. Jest to problem typu zero-one-infinity: jako ktoś definiujący funkcję do wykorzystania przez innych (w tym twoje przyszłe ja), nie chcesz, aby każdy klient twojej funkcji dostosowywał się do każdej zmiany w implementacji twojego funkcji, w tym jakie błędy może zgłosić. Chcesz, aby kod wywołujący twoją funkcję był odporny na takie zmiany.

Ponieważ Twoja funkcja nie może określić, jakie błędy zgłasza (lub może generować w przyszłości), catchbloki, które wychwytują błędy, nie wiedzą, jakie typy błędów może generować. Tak więc, oprócz obsługi typów błędów, o których wiesz, musisz obsłużyć te, których nie znasz, za pomocą catchinstrukcji uniwersalnej - w ten sposób, jeśli twoja funkcja zmieni zestaw błędów, które zgłosi w przyszłości, wywołania nadal będą łapać błędy.

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)
}

Ale nie poprzestawajmy na tym. Pomyśl trochę więcej o tej idei odporności. Sposób, w jaki zaprojektowałeś swoją kanapkę, musisz opisać błędy w każdym miejscu, w którym ich używasz. Oznacza to, że za każdym razem, gdy zmieniasz zestaw przypadków błędów, musisz zmieniać każde miejsce, które ich używa ... niezbyt zabawne.

Ideą definiowania własnych typów błędów jest umożliwienie scentralizowania takich rzeczy. Możesz zdefiniować descriptionmetodę dla swoich błędów:

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

A następnie kod obsługi błędów może poprosić o opisanie typu błędu - teraz każde miejsce, w którym obsługujesz błędy, może używać tego samego kodu i obsługiwać również możliwe przyszłe przypadki błędów.

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

Utoruje to również drogę typom błędów (lub ich rozszerzeniom) do obsługi innych sposobów zgłaszania błędów - na przykład możesz mieć rozszerzenie typu błędu, które wie, jak przedstawić, UIAlertControlleraby zgłosić błąd użytkownikowi iOS.

rickster
źródło
1
@rickster: Czy naprawdę możesz odtworzyć błąd kompilatora? Oryginalny kod kompiluje się bez błędów i ostrzeżeń. A jeśli wyrzucony zostanie nieobjęty wyjątek, program przerywa działanie. error caught in main()- Więc chociaż wszystko, co powiedziałeś, brzmi rozsądnie, nie mogę odtworzyć tego zachowania.
Martin R
5
Uwielbiam sposób, w jaki oddzieliłeś komunikaty o błędach w rozszerzeniu. Naprawdę fajny sposób na utrzymanie kodu w czystości! Świetny przykład!
Konrad77
Zdecydowanie zaleca się unikanie używania trywyrażenia wymuszonego w kodzie produkcyjnym, ponieważ może to spowodować błąd w czasie wykonywania i spowodować awarię aplikacji
Otar
@Otar ogólnie dobra myśl, ale to trochę nie na temat - odpowiedź nie dotyczy używania (lub nieużywania) try!. Ponadto prawdopodobnie istnieją ważne, „bezpieczne” przypadki użycia dla różnych operacji „wymuszania” w języku Swift (rozpakowywanie, próba itp.), Nawet w przypadku kodu produkcyjnego - jeśli dzięki warunkom wstępnym lub konfiguracji niezawodnie wyeliminujesz możliwość awarii, może bardziej rozsądne jest zwarcie do natychmiastowej awarii niż pisanie kodu obsługi błędów, którego nie można przetestować.
rickster
Jeśli potrzebujesz tylko wyświetlania komunikatu o błędzie, umieszczenie tej logiki w SandwichErrorklasie ma sens. Jednak podejrzewam, że w przypadku większości błędów logika obsługi błędów nie może być tak hermetyzowana. Dzieje się tak, ponieważ zwykle wymaga to znajomości kontekstu wywołującego (czy przywrócić, spróbować ponownie, zgłosić awarię nadawcy itp.). Innymi słowy, podejrzewam, że i tak najpowszechniejszym wzorcem byłoby dopasowanie do określonych typów błędów.
maksymalnie
29

Podejrzewam, że to po prostu nie zostało jeszcze poprawnie wdrożone. Przewodnik szybkiego programowania zdecydowanie wydaje się sugerować, że kompilator może wywnioskować wyczerpujące dopasowania „jak instrukcja przełącznika”. Nie wspomina o potrzebie generała catch, aby być wyczerpującym.

Zauważysz również, że błąd występuje w trywierszu, a nie na końcu bloku, tj. W pewnym momencie kompilator będzie w stanie wskazać, która tryinstrukcja w bloku ma nieobsługiwane typy wyjątków.

Dokumentacja jest jednak nieco niejednoznaczna. Przejrzałem wideo „Co nowego w Swift” i nie znalazłem żadnych wskazówek; Będę próbował dalej.

Aktualizacja:

Jesteśmy teraz w wersji Beta 3 bez cienia wnioskowania o typie błędu. Teraz uważam, że gdyby było to kiedykolwiek zaplanowane (i nadal myślę, że tak było w pewnym momencie), dynamiczne wysyłanie rozszerzeń protokołów prawdopodobnie to zabiło.

Aktualizacja Beta 4:

Xcode 7b4 dodał obsługę komentarzy w dokumencie Throws:, które „powinny być używane do dokumentowania, jakie błędy mogą być zgłaszane i dlaczego”. Myślę, że to przynajmniej zapewnia pewien mechanizm komunikowania błędów konsumentom API. Kto potrzebuje systemu typów, skoro masz dokumentację!

Kolejna aktualizacja:

Po spędzeniu pewnego czasu z nadzieją do automatycznego ErrorTypewnioskowania, a obecnie pracuje co ograniczenia byłyby z tego modelu, ja zmieniłem zdanie - to jest to, co mam nadzieję, że zamiast narzędzi firmy Apple. Głównie:

// 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

Jeszcze inna aktualizacja

Uzasadnienie firmy Apple dotyczące obsługi błędów jest teraz dostępne tutaj . Było również kilka interesujących dyskusji na liście mailingowej swift-evolution . Zasadniczo John McCall jest przeciwny błędom typograficznym, ponieważ wierzy, że większość bibliotek i tak będzie zawierała ogólny przypadek błędu, a wpisane błędy raczej nie dodadzą zbyt wiele do kodu poza szablonem (użył terminu „aspiracyjny blef”). Chris Lattner powiedział, że jest otwarty na błędy maszynowe w Swift 3, jeśli może działać z modelem odporności.

Sam
źródło
Dzięki za linki. Jednak nie przekonał go John: „wiele bibliotek zawiera typ„ inny błąd ”” nie oznacza, że ​​każdy potrzebuje typu „inny błąd”.
Franklin Yu
Oczywistym przeciwnikiem jest to, że nie ma prostego sposobu, aby dowiedzieć się, jaki rodzaj błędu zostanie zgłoszony przez funkcję, dopóki tak się nie stanie, zmuszając programistę do wychwycenia wszystkich błędów i spróbowania ich jak najlepszego obsłużenia. Szczerze mówiąc, jest to dość denerwujące.
William T Froggard
4

Swift martwi się, że zestawienie sprawy nie obejmuje wszystkich spraw, aby to naprawić, musisz utworzyć domyślną sprawę:

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
źródło
2
Ale czy to nie jest niezręczne? Mam tylko dwa przypadki i wszystkie z nich są wymienione w catchzestawieniach.
mustafa
2
Teraz jest dobry moment na prośbę o ulepszenie, która dodaje func method() throws(YourErrorEnum), a nawet throws(YourEnum.Error1, .Error2, .Error3)wie, co można wyrzucić
Matthias Bauch
8
Zespół kompilatora Swift na jednej z sesji WWDC dał jasno do zrozumienia, że ​​nie chce pedantycznych list wszystkich możliwych błędów „takich jak Java”.
Sam
4
Nie ma błędu domyślnego / domyślnego; po prostu zostaw puste miejsce {}, jak wskazywały inne plakaty
Opus1217,
1
@Icaro To nie czyni mnie bezpiecznym; Jeśli w przyszłości „dodam nowy wpis do tablicy”, kompilator powinien wrzeszczeć na mnie, że nie aktualizuję wszystkich klauzul catch, na które ma to wpływ.
Franklin Yu
3

Byłem również rozczarowany brakiem typu, jaki funkcja może rzucać, ale teraz dostaję to dzięki @rickster i podsumuję to w ten sposób: powiedzmy, że moglibyśmy określić typ, który funkcja rzuca, mielibyśmy coś takiego:

enum MyError: ErrorType { case ErrorA, ErrorB }

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

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

Problem polega na tym, że nawet jeśli nic nie zmienimy w myFunctionThatThrows, jeśli dodamy po prostu przypadek błędu do MyError:

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

mamy przerąbane, ponieważ nasze polecenie do / try / catch nie jest już wyczerpujące, podobnie jak każde inne miejsce, w którym nazwaliśmy funkcje, które wyrzucają MyError

greg3z
źródło
3
Nie wiem, czy rozumiem, dlaczego masz przerąbane. Otrzymałbyś błąd kompilatora, czego chcesz, prawda? Dzieje się tak w przypadku przełączania instrukcji, jeśli dodasz wielkość wyliczenia.
Sam,
W pewnym sensie wydawało mi się najbardziej prawdopodobne, że stanie się to z błędem wyliczenia / do przypadku, ale jest dokładnie tak, jak w przypadku wyliczeń / przełącznika, masz rację. Wciąż próbuję przekonać samego siebie, że wybór Apple, aby nie pisać tego, co rzucamy, jest dobry, ale nie pomagasz mi w tym! ^^
greg3z
Ręczne wpisywanie wyrzuconych błędów kończy się wielkim bałaganem w nietrywialnych przypadkach. Typy są sumą wszystkich możliwych błędów ze wszystkich instrukcji throw i try w ramach funkcji. Jeśli ręcznie obsługujesz wyliczenia błędów, będzie to bolesne. Jednak catch {}na dole każdego bloku jest prawdopodobnie gorszy. Mam nadzieję, że kompilator w końcu automatycznie wywnioskuje typy błędów tam, gdzie może, ale nie byłem w stanie potwierdzić.
Sam
Zgadzam się, że kompilator powinien teoretycznie być w stanie wywnioskować typy błędów zgłaszane przez funkcję. Ale myślę, że dla programisty sensowne jest również wyraźne zapisanie ich dla jasności. W nietrywialnych przypadkach, o których mówisz, wypisywanie różnych typów błędów wydaje mi się w porządku: func f () zgłasza ErrorTypeA, ErrorTypeB {}
greg3z
Zdecydowanie brakuje dużej części w tym, że nie ma mechanizmu komunikowania typów błędów (innych niż komentarze do dokumentów). Jednak zespół Swift powiedział, że nie chce jawnych list typów błędów. Jestem pewien, że większość osób, które w przeszłości miały do ​​czynienia z wyjątkami sprawdzanymi przez Javę, zgodzi się 😀
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
}

Teraz potwierdź numer:

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

Utwórz wyliczenie w ten sposób:

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

Utwórz metodę taką jak:

 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
}

Teraz sprawdź, czy błąd występuje, czy nie, i napraw go:

do{
try calculateSpending(morningSpending: 60, eveningSpending: 50)
} catch spendingError.minus{
print("This is not possible...")
} catch spendingError.limit{
print("Limit reached...")
}
Panie Javed Multani
źródło
Blisko, ale bez cygara. Spróbuj naprawić odstępy i zrobić obudowę wielbłąda enum.
Alec