How do I tell which guard statement failed?

前端 未结 7 1656
醉梦人生
醉梦人生 2020-12-28 17:58

If I’ve got a bunch of chained guard let statements, how can I diagnose which condition failed, short of breaking apart my guard let into multiple statements?

Given

相关标签:
7条回答
  • 2020-12-28 18:37

    The simplest thing I can think of is to break out the statements into 4 sequential guard else statements, but that feels wrong.

    In my personal opinion, the Swift way shouldn't require you to check whether the values are nil or not.

    However, you could extend Optional to suit your needs:

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

    Allowing for:

    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
    }
    
    0 讨论(0)
  • 2020-12-28 18:38

    One possible (non-idiomatic) workaround: make use of the where clause to track the success of each subsequent optional binding in the guard block

    I see nothing wrong with splitting up your guard statements in separate guard blocks, in case you're interested in which guard statement that fails.

    Out of a technical perspective, however, one alternative to separate guard blocks is to make use of a where clause (to each optional binding) to increment a counter each time an optional binding is successful. In case a binding fails, the value of the counter can be used to track for which binding this was. E.g.:

    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
    

    Above we make use of the fact that the result of an assignment is the empty tuple (), whereas the side effect is the assignment to the lhs of the expression.

    If you'd like to avoid introducing the mutable counter i prior the scope of guard clause, you could place the counter and the incrementing of it as a static class member, e.g.

    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
    

    Possibly a more natural approach is to propagate the value of the counter by letting the function throw an 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)")
    }
    

    I should probably point out, however, that the above is probably closer to be categorized as a "hacky" construct, rather than an idiomatic one.

    0 讨论(0)
  • 2020-12-28 18:51

    I think other answers here are better, but another approach is to define functions like this:

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

    And then use it like this

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

    You could extend it to print the file & line number of the guard statement like other answers.

    On the plus side, there isn't too much clutter at the call site, and you only get output for the failing cases. But since it uses tuples and you can't write a function that operates on arbitrary tuples, you would have to define a similar method for one parameter, two parameters etc up to some arity. It also breaks the visual relation between the clause and the variable it's being bound to, especially if the unwrapped clauses are long.

    0 讨论(0)
  • 2020-12-28 18:57

    Very good question

    I wish I had a good answer for that but I have not.

    Let's begin

    However let's take a look at the problem together. This is a simplified version of your function

    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
    }
    

    Inside the else we don't know what did go wrong

    First of all inside the else block we do NOT have access to the constants defined in the guard statement. This because the compiler doesn't know which one of the clauses did fail. So it does assume the worst case scenario where the first clause did fail.

    Conclusion: we cannot write a "simple" check inside the else statement to understand what did not work.

    Writing a complex check inside the else

    Of course we could replicate inside the else the logic we put insito the guard statement to find out the clause which did fail but this boilerplate code is very ugly and not easy to maintain.

    Beyond nil: throwing errors

    So yes, we need to split the guard statement. However if we want a more detailed information about what did go wrong our foo function should no longer return a nil value to signal an error, it should throw an error instead.

    So

    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
    }
    

    I am curious about what the community thinks about this.

    0 讨论(0)
  • 2020-12-28 19:00

    Erica Sadun just wrote a good blog post on this exact topic.

    Her solution was to hi-jack the where clause and use it to keep track of which guard statements pass. Each successful guard condition using the diagnose method will print the file name and the line number to the console. The guard condition following the last diagnose print statement is the one that failed. The solution looked like this:

    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 {
            // ...
    }
    

    Erica's write-up on this topic can be found here

    0 讨论(0)
  • 2020-12-28 19:01

    Normally, a guard statement doesn't let you distinguish which of its conditions wasn't satisfied. Its purpose is that when the program executes past the guard statement, you know all the variables are non-nil. But it doesn't provide any values inside the guard/else body (you just know that the conditions weren't all satisfied).

    That said, if all you want to do is print something when one of the steps returns nil, you could make use of the coalescing operator ?? to perform an extra action.

    Make a generic function that prints a message and returns 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
    }
    

    Then use this function as a "fallback" for each case. Since the ?? operator employs short-circuit evaluation, the right-hand side won't be executed unless the left-hand side has already returned nil.

    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
    }
    0 讨论(0)
提交回复
热议问题