How do I tell which guard statement failed?

2019-02-03 11:19发布

问题:

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 this example:

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
    }

How can I tell which of the 4 let statements was the one that failed and invoked the else block?

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

 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
    }

If I wanted to keep them all in one guard statement, I can think of another option. Checking for nils inside the guard statement might work:

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
    }

I don't even know if that will compile, but then I might as well have used a bunch of if let statements or guards to begin with.

What's the idiomatic Swift way?

回答1:

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



回答2:

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
}


回答3:

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.



回答4:

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.



回答5:

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
}


回答6:

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.