如何判断哪个警卫声明失败?

时间:2016-06-06 18:23:11

标签: swift error-handling control-flow

如果我有一堆链式守卫让我们发表声明,我怎样才能诊断出哪个条件失败了,不能将我的警卫分成多个陈述?

给出这个例子:

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 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语句中的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方式?

8 个答案:

答案 0 :(得分:11)

Erica Sadun刚刚写了一篇关于这个话题的好文章。

她的解决方案是对where子句进行hi-jack并使用它来跟踪哪些保护语句通过。使用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

找到

答案 1 :(得分:8)

通常,guard语句不能让您区分哪些条件不满足。其目的是当程序执行过去保护语句时,您知道所有变量都是非零的。但它并没有提供内部守护/ else正文中的任何值(你只知道条件并非全部满足)。

也就是说,如果其中一个步骤返回print,您想要做的只是nil,那么您可以使用合并运算符??来执行额外的操作。

创建一个打印消息的泛型函数并返回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
}

然后将此功能用作&#34;后备&#34;对于每个案例。由于??运算符使用short-circuit evaluation,因此除非左侧已经返回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
}

答案 2 :(得分:2)

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

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

如果您对哪个保护声明失败感兴趣,我认为在单独的保护区中拆分保护声明没有错。

然而,从技术角度来看,单独guard块的一种替代方法是使用where子句(对每个可选绑定)每次可选绑定成功时递增计数器。如果绑定失败,计数器的值可用于跟踪这是什么绑定。 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

上面我们使用了一个事实,即赋值的结果是空元组(),而副作用是对lhs的赋值表达。

如果你想避免在i子句的范围之前引入可变计数器guard,你可以将计数器和它的递增放置为静态类成员,例如

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”结构,而不是惯用结构。

答案 3 :(得分:2)

非常好的问题

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

让我们开始

但是让我们一起来看看这个问题。这是您的功能的简化版本

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块内的 NOT 可以访问guard语句中定义的常量。这是因为编译器不知道哪个子句失败了。所以它确实假设第一个子句失败的最坏情况。

结论:我们不能在else语句中写一个“简单”的检查来理解什么不起作用。

在else

中写一个复杂的检查

当然,我们可以在else内部复制我们放入保护语句的逻辑,以找出确实失败的子句,但这个样板代码非常难看并且不易维护。

超越零:抛出错误

所以是的,我们需要分开守卫声明。但是,如果我们想要更详细地了解哪些内容出错,我们的foo函数不应再返回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
}

我很好奇社区对此的看法。

答案 4 :(得分:1)

  

我能想到的最简单的事情是将语句分成4个连续的保护其他语句,但这感觉不对。

在我个人看来,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
}

答案 5 :(得分:0)

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

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

你可以扩展它来打印文件&amp;保护声明的行号与其他答案一样。

从好的方面来说,呼叫站点没有太多的混乱,你只能得到失败案例的输出。但是因为它使用元组并且你不能编写一个对任意元组进行操作的函数,所以你必须为一个参数,两个参数等定义一个类似的方法,直到某些arity。它还打破了子句与其绑定的变量之间的视觉关系,特别是如果解开的子句很长。

答案 6 :(得分:0)

我的两分钱:
由于Swift不允许我在后卫保护区中添加buildCustomBottomNavigationItem: (key, item, selected) => Container( color: Color(0xffb79eb5).withOpacity(.70), child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[ Icon( item.icon, size: 18, color: selected ? Colors.white : Colors.black38, ), Text( item.text, style: TextStyle(fontSize: 16, color: selected ? Colors.white : Colors.black38), ), ], ), ), ,因此我想出了以下解决方案:

where

答案 7 :(得分:0)

此代码可用于所有保护和 if 逻辑测试,例如 optional、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()
}