unreachable try thrown example errors error catch because are swift error-handling control-flow

thrown - try catch swift 4 example



¿Cómo puedo saber qué declaración de guardia falló? (6)

Si tengo un montón de declaraciones encadenadas de guardia, ¿cómo puedo diagnosticar qué condición falló, salvo romper mi guardia y dejar entrar varias afirmaciones?

Dado este ejemplo:

guard let keypath = dictionary["field"] as? String, let rule = dictionary["rule"] as? String, let comparator = FormFieldDisplayRuleComparator(rawValue: rule), let value = dictionary["value"] else { return nil }

¿Cómo puedo saber cuál de las 4 declaraciones de let fue la que falló e invoqué el bloque else?

Lo más simple que se me ocurre es dividir las declaraciones en 4 declaraciones secuenciales de guardias, pero eso parece estar mal.

guard let keypath = dictionary["field"] as? String else { print("Keypath failed to load.") self.init() return nil } guard let rule = dictionary["rule"] as? String else { print("Rule failed to load.") self.init() return nil } guard let comparator = FormFieldDisplayRuleComparator(rawValue: rule) else { print("Comparator failed to load for rawValue: /(rule)") self.init() return nil } guard let value = dictionary["value"] else { print("Value failed to load.") self.init() return nil }

Si quería mantenerlos todos en una declaración de guardia, puedo pensar en otra opción. La comprobación de nils dentro de la declaración de guardia podría funcionar:

guard let keypath = dictionary["field"] as? String, let rule = dictionary["rule"] as? String, let comparator = FormFieldDisplayRuleComparator(rawValue: rule), let value = dictionary["value"] else { if let keypath = keypath {} else { print("Keypath failed to load.") } // ... Repeat for each let... return nil }

Ni siquiera sé si se compilará, pero para empezar, también podría haber usado un montón de declaraciones de guard o guard .

¿Cuál es la forma idiomática de Swift?


Lo más simple que se me ocurre es dividir las declaraciones en 4 declaraciones secuenciales de guardias, pero eso parece estar mal.

En mi opinión personal, el modo Swift no debería exigirle que verifique si los valores son nil o no.

Sin embargo, puede ampliar Optional para satisfacer sus necesidades:

extension Optional { public func testingForNil<T>(@noescape f: (Void -> T)) -> Optional { if self == nil { f() } return self } }

Permitiendo:

guard let keypath = (dictionary["field"] as? String).testingForNil({ /* or else */ }), let rule = (dictionary["rule"] as? String).testingForNil({ /* or else */ }), let comparator = FormFieldDisplayRuleComparator(rawValue: rule).testingForNil({ /* or else */ }), let value = dictionary["value"].testingForNil({ /* or else */ }) else { return nil }


Muy buena pregunta

Ojalá tuviera una buena respuesta para eso, pero no lo hice.

Vamos a empezar

Sin embargo, echemos un vistazo al problema juntos. Esta es una versión simplificada de tu función

func foo(dictionary:[String:AnyObject]) -> AnyObject? { guard let a = dictionary["a"] as? String, b = dictionary[a] as? String, c = dictionary[b] else { return nil // I want to know more ☹️ !! } return c }

Dentro de lo demás, no sabemos qué salió mal

Antes que nada dentro del bloque else NO tenemos acceso a las constantes definidas en la declaración de guard . Esto porque el compilador no sabe cuál de las cláusulas falló. Entonces asume el peor escenario donde falló la primera cláusula.

Conclusión: no podemos escribir un cheque "simple" dentro de la declaración else para entender lo que no funcionó.

Escribir un cheque complejo dentro del otro

Por supuesto, podríamos replicar dentro de lo else la lógica que ponemos en la declaración de guardia para descubrir la cláusula que falló, pero este código repetitivo es muy feo y no es fácil de mantener.

Más allá de nada: lanzar errores

Entonces sí, tenemos que dividir la declaración de guardia. Sin embargo, si queremos una información más detallada sobre lo que salió mal, nuestra función foo ya no debería devolver un valor nil para señalar un error, sino que debería generar un error.

Asi que

enum AppError: ErrorType { case MissingValueForKey(String) } func foo(dictionary:[String:AnyObject]) throws -> AnyObject { guard let a = dictionary["a"] as? String else { throw AppError.MissingValueForKey("a") } guard let b = dictionary[a] as? String else { throw AppError.MissingValueForKey(a) } guard let c = dictionary[b] else { throw AppError.MissingValueForKey(b) } return c }

Tengo curiosidad sobre lo que piensa la comunidad sobre esto.


Una solución posible (no idiomática): utilice la cláusula where para seguir el éxito de cada enlace opcional posterior en el bloque de guard

No veo nada de malo en dividir las declaraciones de tu guardia en bloques de guardia separados, en caso de que estés interesado en qué declaración de guardia falla.

Desde una perspectiva técnica, sin embargo, una alternativa para separar bloques de guard es hacer uso de una cláusula where (para cada enlace opcional) para incrementar un contador cada vez que se realiza un enlace opcional exitoso. En caso de que un enlace falle, el valor del contador se puede usar para rastrear para qué enlace fue esto. P.ej:

func foo(a: Int?, _ b: Int?) { var i: Int = 1 guard let a = a where (i+=1) is (), let b = b where (i+=1) is () else { print("Failed at condition #/(i)") return } } foo(nil,1) // Failed at condition #1 foo(1,nil) // Failed at condition #2

Arriba hacemos uso del hecho de que el resultado de una asignación es la tupla vacía () , mientras que el efecto secundario es la asignación a la lhs de la expresión.

Si desea evitar introducir el contador mutable i antes del alcance de la cláusula de guard , puede colocar el contador y aumentarlo como un miembro de clase estático, por ejemplo

class Foo { static var i: Int = 1 static func reset() -> Bool { i = 1; return true } static func success() -> Bool { i += 1; return true } } func foo(a: Int?, _ b: Int?) { guard Foo.reset(), let a = a where Foo.success(), let b = b where Foo.success() else { print("Failed at condition #/(Foo.i)") return } } foo(nil,1) // Failed at condition #1 foo(1,nil) // Failed at condition #2

Posiblemente un enfoque más natural es propagar el valor del contador dejando que la función arroje un error:

class Foo { /* as above */ } enum Bar: ErrorType { case Baz(Int) } func foo(a: Int?, _ b: Int?) throws { guard Foo.reset(), let a = a where Foo.success(), let b = b where Foo.success() else { throw Bar.Baz(Foo.i) } // ... } do { try foo(nil,1) // Baz error: failed at condition #1 // try foo(1,nil) // Baz error: failed at condition #2 } catch Bar.Baz(let num) { print("Baz error: failed at condition #/(num)") }

Debo señalar, sin embargo, que lo anterior probablemente esté más cerca de ser categorizado como un constructo "hacky", en lugar de uno idiomático.


Creo que otras respuestas aquí son mejores, pero otro enfoque es definir funciones como esta:

func checkAll<T1, T2, T3>(clauses: (T1?, T2?, T3?)) -> (T1, T2, T3)? { guard let one = clauses.0 else { print("1st clause is nil") return nil } guard let two = clauses.1 else { print("2nd clause is nil") return nil } guard let three = clauses.2 else { print("3rd clause is nil") return nil } return (one, two, three) }

Y luego úsalo así

let a: Int? = 0 let b: Int? = nil let c: Int? = 3 guard let (d, e, f) = checkAll((a, b, c)) else { fatalError() } print("a: /(d)") print("b: /(e)") print("c: /(f)")

Puede extenderlo para imprimir el número de archivo y línea de la declaración de guardia como otras respuestas.

En el lado positivo, no hay mucho desorden en el sitio de llamadas, y solo se obtiene salida para los casos con fallas. Pero como usa tuplas y no puede escribir una función que opere en tuplas arbitrarias, debería definir un método similar para un parámetro, dos parámetros, etc. hasta cierto punto. También rompe la relación visual entre la cláusula y la variable a la que está obligado, especialmente si las cláusulas desenvueltas son largas.


Erica Sadun acaba de escribir una buena publicación de blog sobre este tema exacto.

Su solución fue conectar la cláusula where y usarla para hacer un seguimiento de qué declaraciones de guardia pasan. Cada condición de guardia exitosa utilizando el método de diagnose imprimirá el nombre del archivo y el número de línea en la consola. La condición de guardia después de la última declaración de impresión de diagnose es la que falló. La solución se veía así:

func diagnose(file: String = #file, line: Int = #line) -> Bool { print("Testing /(file):/(line)") return true } // ... let dictionary: [String : AnyObject] = [ "one" : "one" "two" : "two" "three" : 3 ] guard // This line will print the file and line number let one = dictionary["one"] as? String where diagnose(), // This line will print the file and line number let two = dictionary["two"] as? String where diagnose(), // This line will NOT be printed. So it is the one that failed. let three = dictionary["three"] as? String where diagnose() else { // ... }

La reseña de Erica sobre este tema se puede encontrar aquí


Normalmente, una declaración de guard no le permite distinguir cuál de sus condiciones no se cumplió. Su propósito es que cuando el programa se ejecuta después de la instrucción de guardia, usted sabe que todas las variables son no-nil. Pero no proporciona ningún valor dentro del cuerpo de guard / else (simplemente sabe que las condiciones no fueron todas satisfechas).

Dicho esto, si todo lo que quiere hacer es print algo cuando uno de los pasos devuelve nil , ¿podría utilizar el operador de fusión ?? para realizar una acción extra.

Realice una función genérica que imprima un mensaje y devuelva nil :

/// Prints a message and returns `nil`. Use this with `??`, e.g.: /// /// guard let x = optionalValue ?? printAndFail("missing x") else { /// // ... /// } func printAndFail<T>(message: String) -> T? { print(message) return nil }

A continuación, utilice esta función como un "respaldo" para cada caso. Desde el ?? el operador emplea la evaluación de cortocircuito , el lado derecho no se ejecutará a menos que el lado izquierdo ya haya devuelto cero.

guard let keypath = dictionary["field"] as? String ?? printAndFail("missing keypath"), let rule = dictionary["rule"] as? String ?? printAndFail("missing rule"), let comparator = FormFieldDisplayRuleComparator(rawValue: rule) ?? printAndFail("missing comparator"), let value = dictionary["value"] ?? printAndFail("missing value") else { // ... return }