J'essaie de comprendre le nouveau problème de gestion des erreurs dans Swift 2. Voici ce que j'ai fait: J'ai d'abord déclaré une erreur enum:
enum SandwichError: ErrorType {
case NotMe
case DoItYourself
}
Et puis j'ai déclaré une méthode qui renvoie une erreur (pas une exception. 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é des appels. 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 compilateur de ligne do
dit Errors thrown from here are not handled because the enclosing catch is not exhaustive
. Mais à mon avis, il est exhaustif car il n’ya que deux cas en SandwichError
enum.
Pour les instructions de commutation standard, Swift peut comprendre qu'il est exhaustif lorsque chaque cas est traité.
Le modèle de traitement des erreurs Swift 2 comporte deux points importants: l'exhaustivité et la résilience. Ensemble, ils se résument à votre déclaration do
/catch
, qui doit capturer toutes les erreurs possibles, pas seulement celles que vous savez pouvoir générer.
Notez que vous ne déclarez pas les types d'erreur qu'une fonction peut générer, mais uniquement si elle génère des erreurs. C'est un problème du type zéro-un-infini: en tant que personne définissant une fonction pour les autres (y compris votre futur moi), vous ne voulez pas obliger chaque client de votre fonction à s'adapter à chaque changement dans la mise en œuvre de votre fonction, y compris quelles erreurs il peut jeter. Vous voulez que le code qui appelle votre fonction soit résistant à un tel changement.
Étant donné que votre fonction ne peut pas indiquer le type d'erreur qu'elle génère (ou risque de générer ultérieurement), les blocs catch
qui capturent les erreurs ne savent pas quels types d'erreurs sont susceptibles d'être générés. Ainsi, en plus de gérer les types d'erreur que vous connaissez, vous devez gérer celles que vous ne connaissez pas avec une instruction universelle catch
. Ainsi, si votre fonction modifie le jeu d'erreurs qu'elle génère à l'avenir, les appelants captureront toujours ses 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 à cette idée de résilience un peu plus. Dans la manière 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 modifier tous les emplacements qui les utilisent ... pas très amusant.
L'idée derrière vos propres types d'erreur est de vous permettre de centraliser des choses comme ça. Vous pouvez définir une méthode description
pour vos erreurs:
extension SandwichError: CustomStringConvertible {
var description: String {
switch self {
case NotMe: return "Not me error"
case DoItYourself: return "Try Sudo"
}
}
}
Et ensuite, votre code de gestion des erreurs peut demander à votre type d'erreur de se décrire lui-même. Désormais, chaque endroit où vous gérez des erreurs peut utiliser le même code et gérer les éventuels cas d'erreur futurs.
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 aux types d'erreur (ou aux extensions qui les utilisent) pour prendre en charge d'autres moyens de signaler les erreurs - par exemple, vous pouvez avoir une extension sur votre type d'erreur qui sait comment présenter un UIAlertController
pour signaler l'erreur à un utilisateur iOS. .
Je suppose que cela n’a pas encore été mis en œuvre correctement. Le Guide de programmation Swift semble bien impliquer que le compilateur peut déduire des correspondances exhaustives 'comme une instruction switch'. Il n’est pas fait mention de la nécessité d’un code général catch
pour être exhaustif.
Vous remarquerez également que l'erreur se trouve sur la ligne try
, pas à la fin du bloc, c'est-à-dire que le compilateur sera capable de localiser à quel moment l'instruction try
du bloc contient des types d'exception non gérés. .
La documentation est un peu ambiguë cependant. J’ai feuilleté la vidéo "Quoi de neuf dans Swift" et je n’ai trouvé aucun indice; Je vais continuer à essayer.
Mise à jour:
Nous en sommes maintenant à la bêta 3, sans indice d'inférence ErrorType. Je crois maintenant que si cela avait déjà été planifié (et je pense toujours que c'était à un moment donné), la répartition dynamique sur les extensions de protocole l'a probablement tué.
Mise à jour de la bêta 4:
Xcode 7b4 a ajouté le support des commentaires de 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 permettant de communiquer les erreurs aux consommateurs d'API. Qui a besoin d'un système de type quand vous avez de la documentation!
Une autre mise à jour:
Après avoir passé du temps à espérer une inférence ErrorType
et à déterminer les limites de ce modèle, j'ai changé d'avis - this , c'est ce que j'espère Apple implémente à 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 logique de traitement des erreurs d’Apple est maintenant disponible ici . Des discussions intéressantes ont également eu lieu sur la liste de diffusion Swift-evolution . John McCall est fondamentalement opposé aux erreurs typées 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 typées ajoutent beaucoup au code, mis à part le passe-partout (il a utilisé le terme "aspirational bluff"). Chris Lattner a déclaré qu’il était ouvert aux erreurs typées dans Swift 3 si cela pouvait fonctionner avec le modèle de résilience.
Swift craint que votre déclaration de cas ne couvre pas tous les cas. Pour le résoudre, 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")
}
J'ai aussi é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 suit: disons que nous pourrions spécifier le type qu'une fonction lève, 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 parce que notre do/try/catch n'est plus exhaustif, ainsi que tout autre endroit où nous avons appelé des fonctions qui jettent MyError
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
}
Maintenant, validez le numéro:
do {
let number = try validateEvenNumber(0)
print("Valid Even Number: \(number)")
} catch let error as NumberError {
print(error.description)
}