Syntaxe Swift do-try-catch

162

Je fais un essai pour comprendre la nouvelle gestion des erreurs dans swift 2. Voici ce que j'ai fait: J'ai d'abord déclaré une énumération d'erreur:

enum SandwichError: ErrorType {
    case NotMe
    case DoItYourself
}

Et puis j'ai déclaré une méthode qui lève une erreur (pas une exception les gens. C'est une erreur.). Voici cette méthode:

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

    return sandwich
}

Le problème vient du côté appelant. Voici le code qui appelle cette méthode:

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

Après le docompilateur de ligne dit Errors thrown from here are not handled because the enclosing catch is not exhaustive. Mais à mon avis, il est exhaustif car il n'y a que deux cas SandwichErrorenum.

Pour les instructions de commutation régulières, Swift peut comprendre qu'il est exhaustif lorsque chaque cas est traité.

Mustafa
la source
3
Vous ne spécifiez pas le type d'erreur que vous lancez, donc Swift ne peut pas déterminer toutes les options possibles
Farlei Heinen
Existe-t-il un moyen de spécifier le type d'erreur?
mustafa
Je ne trouve rien dans la nouvelle version du livre Swift - seulement le mot-clé jette pour le moment
Farlei Heinen
Fonctionne pour moi dans un terrain de jeu sans erreurs ni avertissements.
Fogmeister
2
Les terrains de jeu semblent autoriser les doblocs au niveau supérieur qui ne sont pas exhaustifs - si vous enveloppez le faire dans une fonction non-lancer, cela générera l'erreur.
Sam

Réponses:

267

Le modèle de gestion des erreurs Swift 2 présente deux points importants: l'exhaustivité et la résilience. Ensemble, ils se résument à votre déclaration do/ catchdevant attraper toutes les erreurs possibles, pas seulement celles que vous savez que vous pouvez lancer.

Notez que vous ne déclarez pas les types d'erreurs qu'une fonction peut générer, mais uniquement si elle génère du tout. C'est une sorte de problème zéro-un-infini: en tant que personne définissant une fonction pour les autres (y compris votre futur moi) à utiliser, vous ne voulez pas avoir à obliger chaque client de votre fonction à s'adapter à chaque changement dans l'implémentation de votre fonction, y compris les erreurs qu'il peut générer. Vous voulez que le code qui appelle votre fonction résiste à ce changement.

Parce que votre fonction ne peut pas dire quel type d'erreurs elle génère (ou pourrait générer à l'avenir), les catchblocs qui interceptent les erreurs ne savent pas quels types d'erreurs elle pourrait générer. Ainsi, en plus de gérer les types d'erreur que vous connaissez, vous devez gérer ceux que vous ne connaissez pas avec une catchinstruction universelle - de cette façon, si votre fonction modifie l'ensemble des erreurs qu'elle génère à l'avenir, les appelants verront toujours son les erreurs.

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

Mais ne nous arrêtons pas là. Pensez encore à cette idée de résilience. La façon dont vous avez conçu votre sandwich, vous devez décrire les erreurs à chaque endroit où vous les utilisez. Cela signifie que chaque fois que vous modifiez l'ensemble des cas d'erreur, vous devez changer chaque endroit qui les utilise ... pas très amusant.

L'idée derrière la définition de vos propres types d'erreur est de vous permettre de centraliser des choses comme ça. Vous pouvez définir une descriptionméthode pour vos erreurs:

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

Et puis votre code de gestion des erreurs peut demander à votre type d'erreur de se décrire - désormais, chaque endroit où vous gérez des erreurs peut utiliser le même code et gérer d'éventuels futurs cas d'erreur.

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

Cela ouvre également la voie à des types d'erreur (ou à des extensions sur eux) pour prendre en charge d'autres moyens de signaler des erreurs - par exemple, vous pourriez avoir une extension sur votre type d'erreur qui sait comment présenter une UIAlertControllerpour signaler l'erreur à un utilisateur iOS.

Rickster
la source
1
@rickster: Pourriez-vous vraiment reproduire l'erreur du compilateur? Le code original se compile sans erreurs ni avertissements pour moi. Et si une exception non corrigée est lancée, le programme s'interrompt avec error caught in main(). - Donc, bien que tout ce que vous avez dit semble raisonnable, je ne peux pas reproduire ce comportement.
Martin R
5
J'adore la façon dont vous avez séparé les messages d'erreur dans une extension. Un moyen vraiment sympa de garder votre code propre! Excellent exemple!
Konrad77 du
Il est fortement recommandé d'éviter d'utiliser l' tryexpression forcée dans le code de production car elle peut provoquer une erreur d'exécution et provoquer le plantage de votre application
Otar
@Otar a bien pensé en général, mais c'est un peu hors sujet - la réponse ne concerne pas l'utilisation (ou ne pas utiliser) try!. En outre, il existe sans doute des cas d'utilisation valides et «sûrs» pour les diverses opérations de «force» dans Swift (déballer, essayer, etc.) même pour le code de production - si, grâce à la précondition ou à la configuration, vous avez éliminé de manière fiable la possibilité d'échec, cela pourrait être plus raisonnable de court-circuiter en échec instantané que d'écrire du code de gestion des erreurs non testable.
rickster
Si vous n'avez besoin que d'afficher le message d'erreur, il est logique de placer cette logique dans la SandwichErrorclasse. Cependant, je soupçonne que pour la plupart des erreurs, la logique de gestion des erreurs ne peut pas être ainsi encapsulée. En effet, cela nécessite généralement la connaissance du contexte de l'appelant (s'il faut récupérer, réessayer, ou signaler un échec en amont, etc.). En d'autres termes, je soupçonne que le modèle le plus courant devrait de toute façon correspondre à des types d'erreur spécifiques.
max
29

Je soupçonne que cela n'a pas encore été mis en œuvre correctement. Le Guide de programmation Swift semble définitivement impliquer que le compilateur peut déduire des correspondances exhaustives «comme une instruction switch». Il ne fait aucune mention du besoin d'un général catchpour être exhaustif.

Vous remarquerez également que l'erreur se trouve sur la tryligne, et non à la fin du bloc, c'est-à-dire qu'à un moment donné, le compilateur pourra identifier quelle tryinstruction du bloc a des types d'exceptions non gérés.

La documentation est cependant un peu ambiguë. J'ai parcouru la vidéo «Quoi de neuf dans Swift» et je n'ai trouvé aucun indice; Je vais continuer à essayer.

Mettre à jour:

Nous sommes maintenant à la bêta 3 sans aucun indice d'inférence ErrorType. Je crois maintenant que si cela a jamais été planifié (et je pense toujours que c'était à un moment donné), l'envoi dynamique sur les extensions de protocole l'a probablement tué.

Mise à jour bêta 4:

Xcode 7b4 a ajouté la prise en charge des commentaires doc pour Throws:, qui «devrait être utilisé pour documenter quelles erreurs peuvent être générées et pourquoi». Je suppose que cela fournit au moins un mécanisme pour communiquer les erreurs aux consommateurs d'API. Qui a besoin d'un système de typage quand on a de la documentation!

Une autre mise à jour:

Après avoir passé un certain temps en espérant automatiquement l' ErrorTypeinférence, et de travail quelles sont les limites seraient de ce modèle, j'ai changé d' avis - c'est ce que je souhaite d' Apple met en œuvre à la place. Essentiellement:

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

Encore une autre mise à jour

La justification de la gestion des erreurs d'Apple est maintenant disponible ici . Il y a également eu des discussions intéressantes sur la liste de diffusion Swift-evolution . Essentiellement, John McCall est opposé aux erreurs de frappe car il pense que la plupart des bibliothèques finiront par inclure un cas d'erreur générique de toute façon, et qu'il est peu probable que les erreurs de frappe ajoutent beaucoup au code en dehors du passe-partout (il a utilisé le terme `` bluff ambitieux ''). Chris Lattner a déclaré qu'il était ouvert aux erreurs de frappe dans Swift 3 s'il pouvait fonctionner avec le modèle de résilience.

Sam
la source
Merci pour les liens. Pas convaincu par John cependant: "de nombreuses bibliothèques incluent le type" autre erreur "" ne signifie pas que tout le monde a besoin d'un type "autre erreur".
Franklin Yu
Le compteur évident est qu'il n'y a pas de moyen simple de savoir quel type d'erreur une fonction va générer, jusqu'à ce qu'elle le fasse, forçant le développeur à détecter toutes les erreurs et à essayer de les gérer au mieux. C'est plutôt ennuyeux, pour être franc.
William T Froggard
4

Swift craint que votre déclaration de cas ne couvre pas tous les cas, pour y remédier, vous devez créer un cas par défaut:

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
la source
2
Mais n'est-ce pas gênant? Je n'ai que deux cas et tous énumérés dans des catchdéclarations.
mustafa
2
C'est le bon moment pour une demande d'amélioration qui ajoute func method() throws(YourErrorEnum), ou même throws(YourEnum.Error1, .Error2, .Error3)pour savoir ce qui peut être lancé
Matthias Bauch
8
L'équipe de compilation Swift lors de l'une des sessions de la WWDC a clairement indiqué qu'elle ne voulait pas de listes pédantes de toutes les erreurs possibles «comme Java».
Sam
4
Il n'y a pas d'erreur par défaut / par défaut; laissez juste un crochet vide {} comme d'autres affiches l'ont souligné
Opus1217
1
@Icaro Cela ne me met pas en sécurité; si j'ajoute une nouvelle entrée dans le tableau à l'avenir, le compilateur devrait me crier dessus pour ne pas avoir mis à jour toutes les clauses catch affectées.
Franklin Yu
3

J'ai également été déçu par le manque de type qu'une fonction peut lancer, mais je l'obtiens maintenant grâce à @rickster et je vais le résumer comme ceci: disons que nous pourrions spécifier le type qu'une fonction lance, nous aurions quelque chose comme ceci:

enum MyError: ErrorType { case ErrorA, ErrorB }

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

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

Le problème est que même si nous ne changeons rien dans myFunctionThatThrows, si nous ajoutons simplement un cas d'erreur à MyError:

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

nous sommes foutus car notre do / try / catch n'est plus exhaustif, ainsi que tout autre endroit où nous avons appelé des fonctions qui lancent MyError

greg3z
la source
3
Je ne sais pas pourquoi tu es foutu. Vous obtiendrez une erreur de compilation, ce que vous voulez, non? C'est ce qui arrive aux instructions de commutation si vous ajoutez un cas d'énumération.
Sam le
Dans un sens, il me semblait très probable que cela se produirait avec des erreurs enums / do case, mais c'est exactement comme cela se passerait dans enums / switch, vous avez raison. J'essaie toujours de me convaincre que le choix d'Apple de ne pas taper ce que nous lançons est le bon, mais vous ne m'aidez pas sur celui-ci! ^^
greg3z
La saisie manuelle des erreurs générées finirait par être un gros gâchis dans des cas non triviaux. Les types sont une union de toutes les erreurs possibles de toutes les instructions throw et try dans la fonction. Si vous gérez manuellement les énumérations d'erreurs, ce sera douloureux. Un catch {}en bas de chaque bloc est sans doute pire. J'espère que le compilateur finira par déduire automatiquement les types d'erreur là où il le peut, mais je n'ai pas été en mesure de confirmer.
Sam
Je suis d'accord que le compilateur devrait, en théorie, être capable de déduire les types d'erreur qu'une fonction génère. Mais je pense qu'il est également logique que le développeur les écrive explicitement pour plus de clarté. Dans les cas non triviaux dont vous parlez, énumérer les différents types d'erreur me semble correct: func f () lance ErrorTypeA, ErrorTypeB {}
greg3z
Il manque certainement une grande partie du fait qu'il n'y a pas de mécanisme pour communiquer les types d'erreur (autres que les commentaires doc). Cependant, l'équipe Swift a déclaré qu'elle ne voulait pas de listes explicites de types d'erreur. Je suis sûr que la plupart des gens qui ont traité des exceptions vérifiées par Java dans le passé seraient d'accord 😀
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
}

Validez maintenant le numéro:

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

Créez une énumération comme ceci:

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

Créez une méthode comme:

 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
}

Maintenant, vérifiez que l'erreur est là ou non et gérez-la:

do{
try calculateSpending(morningSpending: 60, eveningSpending: 50)
} catch spendingError.minus{
print("This is not possible...")
} catch spendingError.limit{
print("Limit reached...")
}
Monsieur Javed Multani
la source
Proche mais pas de cigare. Essayez de fixer l'espacement et de fabriquer l'étui enum camel.
Alec