我如何判断哪个守卫声明失败了?

How do I tell which guard statement failed?

如果我有一堆链式 guard let 语句,除了将我的 guard let 分解成多个语句外,我如何诊断哪个条件失败?

给出这个例子:

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
    }

如何判断 4 个 let 语句中的哪一个失败并调用了 else 块?

我能想到的最简单的事情是将语句分解为 4 个连续的 guard else 语句,但感觉不对。

 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
    }

如果我想将它们全部放在一个 guard 语句中,我可以想到另一种选择。检查 guard 语句中的 nils 可能有效:

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
    }

我什至不知道这是否会编译,但我还不如使用一堆 if let 语句或 guard 开始。

惯用的 Swift 方式是什么?

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

在我个人看来,Swift 方式不应该要求您检查值是否为 nil

但是,您可以扩展 Optional 以满足您的需要:

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

        return self
    }
}

允许:

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
}

一种可能的(非惯用的)解决方法:使用 where 子句来跟踪 guard

中每个后续可选绑定的成功

我认为将您的 guard 语句拆分为单独的 guard 块没有任何问题,以防您对哪个 guard 语句失败感兴趣。

然而,从技术角度来看,分隔 guard 块的一种替代方法是使用 where 子句(针对每个可选绑定)在每次可选绑定时递增计数器是成功的。如果绑定失败,计数器的值可用于跟踪这是哪个绑定。例如:

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

上面我们利用了赋值的结果是空元组(),而副作用[=37] =] 是对表达式的 lhs 的赋值。

如果您想避免在 guard 子句的范围之前引入可变计数器 i,您可以将计数器及其递增作为静态 class成员,例如

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

可能更自然的方法是通过让函数抛出错误来传播计数器的值:

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

不过,我可能应该指出,上述内容可能更接近于归类为“hacky”结构,而不是惯用结构。

很好的问题

我希望对此有一个好的答案,但我没有。

让我们开始吧

不过还是一起来看看问题吧。这是您的函数的简化版本

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
}

在 else 里面我们不知道哪里出了问题

首先,在 else 块中,我们 NOT 可以访问 guard 语句中定义的常量。这是因为编译器不知道哪个子句失败了。所以它确实假设了第一个子句失败的最坏情况。

结论:我们不能在 else 语句中编写 "simple" 检查来了解什么不起作用。

在 else 中编写一个复杂的检查

当然我们可以在 else 中复制我们放入 guard 语句中的逻辑,以找出确实失败的子句,但是这个样板代码非常丑陋且不易维护。

超过零:抛出错误

所以是的,我们需要拆分 guard 语句。但是,如果我们想要更详细的信息,了解哪里出了问题,我们的 foo 函数不应再 return 一个 nil 值来表示错误,它应该抛出一个错误。

所以

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
}

我很好奇社区对此有何看法。

通常,guard 语句不会让您区分它的哪些条件不满足。它的目的是当程序执行past guard语句时,你知道所有的变量都是非nil的。但是它没有提供任何值 inside guard/else body(你只知道条件没有全部满足)。

就是说,如果您想要做的只是 print 某些步骤 returns nil,您可以使用合并运算符 ??执行额外的操作。

创建一个打印消息的通用函数 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
}

然后将此函数用作每个案例的 "fallback"。由于 ?? 运算符使用 short-circuit evaluation,除非左侧已经返回 nil,否则不会执行右侧。

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

Erica Sadun 刚刚就这个主题写了一篇不错的博客 post。

她的解决方案是劫持 where 子句并使用它来跟踪哪些保护语句通过。使用 diagnose 方法的每个成功的保护条件都会将文件名和行号打印到控制台。最后一个 diagnose 打印语句之后的警戒条件是失败的条件。解决方案如下所示:

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 关于此主题的文章 here

我认为这里的其他答案更好,但另一种方法是这样定义函数:

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

然后像这样使用

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

您可以扩展它以像其他答案一样打印保护语句的文件和行号。

从好的方面来说,调用站点没有太多混乱,您只会获得失败案例的输出。但是由于它使用元组并且您不能编写对任意元组进行操作的函数,因此您必须为一个参数、两个参数等定义一个类似的方法,直到达到某个数量。它还破坏了子句与其绑定到的变量之间的视觉关系,尤其是当展开的子句很长时。

我的两分钱:
由于 Swift 不允许我在 guard let 中添加 where,我想出了这个解决方案:

func validate<T>(_ input: T?, file: String = #file, line: Int = #line) -> T? {
    guard let input = input else {
        print("Nil argument at \(file), line: \(line)")
        return nil
    }
    return input
}


class Model {
    let id: Int
    let name: String
    
    init?(id: Int?, name: String?) {
        guard let id = validate(id),
            let name = validate(name) else {
            return nil
        }
        self.id = id
        self.name = name
    }
}

let t = Model(id: 0, name: "ok") // Not nil
let t2 = Model(id: 0, name: nil) // Nil
let t3 = Model(id: nil, name: "ok") // Nil

此代码可用于所有 guard 和 if 逻辑测试,如可选、bool 和 case 测试。它打印失败的逻辑测试行。

class GuardLogger {
    var lastGoodLine: Int
    var lineWithError: Int { lastGoodLine + 1 }
    var file: String
    var function: String
    
    init(file: String = #file, function: String = #function, line: Int = #line) {
        self.lastGoodLine = line
        self.file = file
        self.function = function
    }
    
    func log(line: Int = #line) -> Bool {
        lastGoodLine = line
        return true
    }
    
    func print() {
        Swift.print([file, function, String(lineWithError)].joined(separator: " "))
    }
}

let testBoolTrue = true
let testBoolFalse = false

let guardLogger = GuardLogger()

guard
    testBoolTrue, guardLogger.log(),
    let testOptionalBoolTrue = Optional(testBoolTrue), guardLogger.log(),
    let selfIsViewController = self as? UIViewController, guardLogger.log(),
    testBoolTrue == false, guardLogger.log() // this fails
else {
    print(guardLogger.lastGoodLine)
    fatalError()
}