在Swift 3中,比较两个闭包的方法是什么?

13
假设您在Swift 3中有两个类型为(Int)->()的闭包,并测试以查看它们是否相同:

假设您在Swift 3中有两个类型为(Int)->()的闭包,并测试它们是否相同:

typealias Baz = (Int)->()
let closure1:Baz = { print("foo \($0)") }
let closure2:Baz = { print("bar \($0)") }

if(closure1 == closure2) {
    print("equal")
}

这段代码无法编译通过,并显示以下信息:

二元运算符“==”不能应用于两个类型为“(Int)->()”的操作数

好的,那么我们如何比较两个相同类型的闭包以确定它们是否相同呢?


你该如何判断两个闭包是否相等?相等意味着可互换,那是否意味着当 func sayHi() { print("hi") } 时,你期望 { print("hi") } == { sayHi() }true?那么如果捕获的变量不是 Equatable 类型呢?我不知道有什么明智的方法可以确定两个闭包之间的相等性。 - Hamish
2
值得注意的是,由于闭包可能会经过各种各样的thunks和优化(例如特殊化和方法体共享),Swift不支持闭包之间的引用相等性(请参见https://dev59.com/wmAf5IYBdhLWcg3w9Wiu)。这将导致结果不可预测。 - Hamish
在什么情况下比较闭包才有意义? - Alexander
1
@CommaToast 但检查它们是否包含相同的指令并不意味着它们“总是返回相同的结果”。在我上面给出的示例中,一个闭包调用print,另一个调用sayHi。在-Onone build中,这些闭包将不包含相同的指令,但它们总是做相同的事情。此外,优化级别会影响您获得的结果(使用内联、专门化等)。正如JeremyP所说,通过“它们总是做相同的事情”来确定相等性需要解决停机问题的方案。 - Hamish
2
关于捕获的变量,它们只有在函数运行后才被捕获一次这种说法是不正确的。实际上,捕获发生在闭包实例化时,而不是运行时。 - JeremyP
显示剩余7条评论
2个回答

11

我相信没有办法确定两个闭包是否相等。

显然,逻辑上的相等检查是行不通的。这将等同于找到停机问题的答案。(只需测试您的代码是否等价于一个永久循环的代码片段。如果是,则不停机。如果不是,则停机。)

理论上,你可能希望使用 === 运算符来测试两个闭包是否完全相同,但当我在 Playground 中尝试时会出现错误。

Playground execution failed: error: MyPlayground.playground:1:20: error: cannot check reference equality of functions; operands here have types '(Int) ->  ()' and '(Int) -> ()'
let bar = closure1 === closure2
          ~~~~~~~~ ^   ~~~~~~~~

经过思考,我确信之所以那样做不起作用是因为你无法确定这些闭包确实相等。闭包不仅仅是代码,还包括创建它的上下文,包括任何捕获的内容。不能检查两个闭包是否相等的原因是没有任何有意义的方式来比较它们。

要理解为什么捕获很重要,请看以下代码。

func giveMeClosure(aString: String) -> () -> String
{
     return { "returning " + aString }
}

let closure1 = giveMeClosure(aString: "foo")
let closure2 = giveMeClosure(aString: "bar")

闭包closure1closure2是否相等?它们都使用相同的代码块。

print(closure1()) // prints "returning foo"
print(closure2()) // prints "returning bar"

所以它们不相等。你可以争论代码和捕获是否相同,但是

func giveMeACount(aString: String) -> () -> Int
{
    return { aString.characters.count }
}

let closure3 = giveMeACount(aString: "foo")
let closure4 = giveMeACount(aString: "bar")

print(closure3()) // prints 3
print(closure4()) // prints 3

显然,这些闭包是相等的。不可能实现任何合理的相等定义来适用于每种情况,因此苹果公司甚至都没有尝试。这比提供在某些情况下错误的不完整实现更安全。


@CommaToast 在某些琐碎的情况下可以判断两个闭包是否不相等这一事实与问题的一般答案无关。在Swift中你不能比较闭包(有很好的理由),这就是故事的结局。 - JeremyP
1
@CommaToast ,在简化的情况下,确定两段代码是否具有相同的行为是可能的。在一般情况下,这是停机问题的转换,是不可判定的。完全将所有可能闭包的集合划分为“可以确定等价”的类别和“不能确定等价”的类别也是停机问题的转换,同样是不可判定的。 - Alexander
1
你无法在Swift中比较闭包,完全不行,@JeremyP? 你可以比较它们的类型,这会大大缩小范围。你添加的第二个新示例有两个显然不同的闭包(它们具有不同的输入值)。正是相等性的这个方面——能够检查,知道两个闭包是相同类型的,它们的输入值和它们访问的捕获作用域的部分是否相等——非常缺乏。闭包是一个黑盒子;你甚至无法只读访问它们的状态。你不能说closure1.$0并查看它的第一个参数,或者closure1.scope... - CommaToast
@Alexander 那么当我将它们归一化空格和符号后,如果我将两个代码片段粘贴在一起,为什么git会知道它们是相同还是不同的呢?如果系统无法保证它们是相同的,我如何可靠地分配 closure1 = closure2?很明显,可以比较闭包的类型,以及其中的机器指令,所以唯一剩下的就是比较范围和捕获,对吧?我们可以通过对 Equatable 的定义施加合理的限制来轻松隔离与停机问题相关的问题。 - CommaToast
1
@CommaToast 首先,我不同意你关于图灵证明的观点。首先,他并不是唯一一个做过这件事的人。阿隆佐·邱奇使用λ演算得出了相同的结果。其次,即使在理论上可以确定两个任意代码片段是否相同,在实践中也没有完成过。 - JeremyP
显示剩余10条评论

11

如果你想追踪自己的闭包并将其用作字典键等,可以使用类似这样的方法:

struct TaggedClosure<P, R>: Equatable, Hashable {
    let id: Int
    let closure: (P) -> R

    static func == (lhs: TaggedClosure, rhs: TaggedClosure) -> Bool {
        return lhs.id == rhs.id
    }

    var hashValue: Int { return id }
}

let a = TaggedClosure(id: 1) { print("foo") }
let b = TaggedClosure(id: 1) { print("foo") }
let c = TaggedClosure(id: 2) { print("bar") }

print("a == b:", a == b) // => true
print("a == c:", a == c) // => false
print("b == c:", b == c) // => false

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接