如何测试函数和闭包的相等性?

97

5
据我所知,您也无法检查元类(例如 MyClass.self)的相等性。 - Jiaaro
1
多播闭包,就像C#中的那样。它们在Swift中必然更丑陋,因为你不能重载(T, U)“运算符”,但我们仍然可以自己创建它们。然而,由于无法通过引用从调用列表中删除闭包,我们需要创建自己的包装类。这很麻烦,也不应该是必要的。 - user652038
2
很棒的问题,但完全不同的事情是:你在 å 上使用变音符号来引用 a 的用法真的很有趣。你在这里探索的是一种约定吗?(我不知道我是否真的喜欢它;但它似乎非常强大,特别是在纯函数式编程中。) - Rob Napier
3
@Bill 我正在将闭包存储在数组中,但无法使用indexOf({$0 == closure}来查找和删除它们。现在我必须重构我的代码,因为我认为这是糟糕的语言设计导致的优化问题。 - Zack Morris
@ZackMorris,我有一个封装的字典,用于解决我在这里描述的问题:http://stackoverflow.com/questions/37155530/how-to-make-a-collection-of-blocks-in-swift - Omegaman
显示剩余3条评论
11个回答

86
克里斯·拉特纳(Chris Lattner)在开发者论坛上写道:
这是一个我们故意不想支持的功能。有许多因素会导致函数指针相等(在Swift类型系统意义上,包括几种闭包)失败或更改,具体取决于优化。如果“===”在函数上被定义,编译器将无法合并相同的方法体、共享thunk,并在闭包中执行某些捕获优化。此外,在一些泛型上下文中,这种相等性将非常令人惊讶,其中您可以获得重新抽象垫片,以将函数的实际签名调整为函数类型所期望的签名。
这意味着您甚至不应尝试比较闭包是否相等,因为优化可能会影响结果。 https://devforums.apple.com/message/1035180#1035180

26
这刚刚咬了我,这有点令人沮丧,因为我一直在数组中存储闭包,现在无法使用indexOf({$0 == closure}删除它们,所以我必须重构代码。在我看来,优化不应该影响语言设计,因此除了matt的回答中现已弃用的@objc_block之外,如果没有快速解决方法,我认为Swift目前无法正确存储和检索闭包。因此,我认为在像Web开发中遇到的回调密集型代码中使用Swift并不合适,而这也是我们最初转向Swift的原因... - Zack Morris
5
在闭包中存储某种标识符,以便稍后可以将其删除。如果您正在使用引用类型,则可以仅存储对对象的引用,否则可以设计自己的标识符系统。您甚至可以设计一种类型,该类型具有闭包和唯一标识符,可以代替普通闭包使用。 - drewag
5
@drewag 是的,有一些变通方法,但是 Zack 是对的。这真的真的很烦人。我理解想要优化,但是如果在代码中存在开发者需要比较某些闭包的地方,那么只需要让编译器不优化这些特定部分即可。或者制作一种编译器的附加功能,使其能够创建不会因为该死的优化而破坏平等签名的比较函数。我们正在谈论的是苹果公司...如果他们可以将 Xeon 集成到 iMac 中,那么他们肯定可以使闭包可比较。别把我逼急了! - CommaToast
1
@CommaToast 你把这些闭包的引用存放在哪里,以便稍后可以从数组中将它们删除?或者您是否重新实例化相同的闭包以将其从数组中删除?对于您来说,是否使用符合 Hashable 的值类型(它可以实现 callAsFunction())具有与闭包相同信息的方式可以工作呢?采用这种方法,甚至可以从数组中删除实例而不必将它们存储在另一个位置并重新创建它们。 - Feuermurmel

11

我进行了大量搜索,似乎没有办法比较函数指针。我得到的最佳解决方案是将函数或闭包封装在可哈希的对象中,例如:

var handler:Handler = Handler(callback: { (message:String) in
            //handler body
}))

2
这绝对是最好的方法。虽然必须要包装和解包闭包会让人烦躁,但它比不确定性、不受支持的脆弱性更好。 - user246672

10

最简单的方法是将块类型指定为@objc_block,然后您可以将其强制转换为与===可比较的任何对象。示例:

最简单的方式是将块类型指定为@objc_block,现在您可以将其转换为可与===进行比较的任何对象。例如:

    typealias Ftype = @convention(block) (s:String) -> ()

    let f : Ftype = {
        ss in
        println(ss)
    }
    let ff : Ftype = {
        sss in
        println(sss)
    }
    let obj1 = unsafeBitCast(f, AnyObject.self)
    let obj2 = unsafeBitCast(ff, AnyObject.self)
    let obj3 = unsafeBitCast(f, AnyObject.self)
    
    println(obj1 === obj2) // false
    println(obj1 === obj3) // true
2021年更新:@objc_block更改为@convention(block)以支持Swift 2.x及之后的版本(这些版本不识别@objc_block)。

1
嘿,我正在尝试使用unsafeBitCast(listener, AnyObject.self) === unsafeBitCast(f, AnyObject.self) ,但是出现了致命错误:无法在不同大小的类型之间进行unsafeBitCast。这个想法是构建一个基于事件的系统,但是removeEventListener方法应该能够检查函数指针。 - freezing_
4
在Swift 2.x中,使用@convention(block)代替@objc_block。好答案! - Gabriel.Massana

5
我也一直在寻找答案,最终我找到了。
你需要的是实际函数指针及其隐藏在函数对象中的上下文。
func peekFunc<A,R>(f:A->R)->(fp:Int, ctx:Int) {
    typealias IntInt = (Int, Int)
    let (hi, lo) = unsafeBitCast(f, IntInt.self)
    let offset = sizeof(Int) == 8 ? 16 : 12
    let ptr  = UnsafePointer<Int>(lo+offset)
    return (ptr.memory, ptr.successor().memory)
}
@infix func === <A,R>(lhs:A->R,rhs:A->R)->Bool {
    let (tl, tr) = (peekFunc(lhs), peekFunc(rhs))
    return tl.0 == tr.0 && tl.1 == tr.1
}

以下是演示:

// simple functions
func genericId<T>(t:T)->T { return t }
func incr(i:Int)->Int { return i + 1 }
var f:Int->Int = genericId
var g = f;      println("(f === g) == \(f === g)")
f = genericId;  println("(f === g) == \(f === g)")
f = g;          println("(f === g) == \(f === g)")
// closures
func mkcounter()->()->Int {
    var count = 0;
    return { count++ }
}
var c0 = mkcounter()
var c1 = mkcounter()
var c2 = c0
println("peekFunc(c0) == \(peekFunc(c0))")
println("peekFunc(c1) == \(peekFunc(c1))")
println("peekFunc(c2) == \(peekFunc(c2))")
println("(c0() == c1()) == \(c0() == c1())") // true : both are called once
println("(c0() == c2()) == \(c0() == c2())") // false: because c0() means c2()
println("(c0 === c1) == \(c0 === c1)")
println("(c0 === c2) == \(c0 === c2)")

请查看以下链接了解为什么以及如何使用它: 正如您所见,它只能检查身份(第二个测试结果为false)。但这应该已经足够了。

6
这种方法在编译器优化的情况下可能不可靠。https://devforums.apple.com/message/1035180#1035180 - drewag
9
这是一种基于未定义实现细节的黑客技巧。使用此方法将导致您的程序产生未定义的结果。 - eonil
9
请注意,这取决于未记录的东西和未公开的实现细节,如果它们发生变化可能会导致您的应用程序崩溃。不建议在生产代码中使用。 - Cristik
这是“clover”,但完全无法工作。我不知道为什么会获得赏金。该语言故意没有函数相等性,目的是为了使编译器可以自由地破坏函数相等性以产生更好的优化效果。 - Alexander
这正是Chris Lattner反对的方法(请参见顶部答案)。 - pipacs

4
这里有一个可能的解决方案(在概念上与“tuncay”答案相同)。关键是定义一个包装某些功能(例如Command)的类:

Swift:

typealias Callback = (Any...)->Void
class Command {
    init(_ fn: @escaping Callback) {
        self.fn_ = fn
    }

    var exec : (_ args: Any...)->Void {
        get {
            return fn_
        }
    }
    var fn_ :Callback
}

let cmd1 = Command { _ in print("hello")}
let cmd2 = cmd1
let cmd3 = Command { (_ args: Any...) in
    print(args.count)
}

cmd1.exec()
cmd2.exec()
cmd3.exec(1, 2, "str")

cmd1 === cmd2 // true
cmd1 === cmd3 // false

Java:

interface Command {
    void exec(Object... args);
}
Command cmd1 = new Command() {
    public void exec(Object... args) [
       // do something
    }
}
Command cmd2 = cmd1;
Command cmd3 = new Command() {
   public void exec(Object... args) {
      // do something else
   }
}

cmd1 == cmd2 // true
cmd1 == cmd3 // false

如果您将其制作成通用的,那么这将会更好。 - Alexander

3
这是一个很好的问题,虽然 Chris Lattner 有意不支持这个功能,但像许多开发者一样,我也无法放弃其他语言中轻松完成此任务的感觉。有很多 unsafeBitCast 的例子,但大多数并没有展示全貌,这里有一个更详细的例子
typealias SwfBlock = () -> ()
typealias ObjBlock = @convention(block) () -> ()

func testSwfBlock(a: SwfBlock, _ b: SwfBlock) -> String {
    let objA = unsafeBitCast(a as ObjBlock, AnyObject.self)
    let objB = unsafeBitCast(b as ObjBlock, AnyObject.self)
    return "a is ObjBlock: \(a is ObjBlock), b is ObjBlock: \(b is ObjBlock), objA === objB: \(objA === objB)"
}

func testObjBlock(a: ObjBlock, _ b: ObjBlock) -> String {
    let objA = unsafeBitCast(a, AnyObject.self)
    let objB = unsafeBitCast(b, AnyObject.self)
    return "a is ObjBlock: \(a is ObjBlock), b is ObjBlock: \(b is ObjBlock), objA === objB: \(objA === objB)"
}

func testAnyBlock(a: Any?, _ b: Any?) -> String {
    if !(a is ObjBlock) || !(b is ObjBlock) {
        return "a nor b are ObjBlock, they are not equal"
    }
    let objA = unsafeBitCast(a as! ObjBlock, AnyObject.self)
    let objB = unsafeBitCast(b as! ObjBlock, AnyObject.self)
    return "a is ObjBlock: \(a is ObjBlock), b is ObjBlock: \(b is ObjBlock), objA === objB: \(objA === objB)"
}

class Foo
{
    lazy var swfBlock: ObjBlock = self.swf
    func swf() { print("swf") }
    @objc func obj() { print("obj") }
}

let swfBlock: SwfBlock = { print("swf") }
let objBlock: ObjBlock = { print("obj") }
let foo: Foo = Foo()

print(testSwfBlock(swfBlock, swfBlock)) // a is ObjBlock: false, b is ObjBlock: false, objA === objB: false
print(testSwfBlock(objBlock, objBlock)) // a is ObjBlock: false, b is ObjBlock: false, objA === objB: false

print(testObjBlock(swfBlock, swfBlock)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: false
print(testObjBlock(objBlock, objBlock)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: true

print(testAnyBlock(swfBlock, swfBlock)) // a nor b are ObjBlock, they are not equal
print(testAnyBlock(objBlock, objBlock)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: true

print(testObjBlock(foo.swf, foo.swf)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: false
print(testSwfBlock(foo.obj, foo.obj)) // a is ObjBlock: false, b is ObjBlock: false, objA === objB: false
print(testAnyBlock(foo.swf, foo.swf)) // a nor b are ObjBlock, they are not equal
print(testAnyBlock(foo.swfBlock, foo.swfBlock)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: true

有趣的部分是Swift可以自由地将SwfBlock转换为ObjBlock,但实际上两个转换后的SwfBlock块始终是不同的值,而ObjBlocks则不会。当我们将ObjBlock转换为SwfBlock时,它们也会发生同样的事情,它们变成了两个不同的值。因此,为了保留引用,应避免这种类型的转换。
我仍在理解整个主题,但我希望能够在类/结构方法上使用@convention(block),所以我提出了一个功能请求,需要赞成或解释为什么这是一个坏主意。我也感到这种方法可能完全不好,如果是这样,有人可以解释一下原因吗?

1
我认为你没有理解Chris Latner为什么不支持这个想法的原因。"我也感觉这种方法可能完全是错误的,如果是这样,有人能解释一下吗?" 因为在优化构建中,编译器可以自由地以许多方式操纵代码,从而破坏函数点等式的思想。举个基本的例子,如果一个函数的主体与另一个函数相同,编译器很可能会将两者重叠在机器代码中,只保留不同的退出点。这减少了重复。 - Alexander
1
基本上,闭包是初始化匿名类对象的方法(就像在Java中一样,但在那里更明显)。这些闭包对象是堆分配的,并存储由闭包捕获的数据,这些数据充当闭包函数的隐式参数。闭包对象持有对操作显式(通过func args)和隐式(通过捕获的闭包上下文)args的函数的引用。虽然函数体可以共享为单个唯一点,但闭包对象的指针不能,因为每组封闭值都有一个闭包对象。 - Alexander
1
当你有 Struct S { func f(_: Int) -> Bool } 时,实际上你拥有一个类型为 (S) -> (Int) -> Bool 的函数 S.f。这个函数是可以共享的,它仅由其显式参数进行参数化。但是,当您将其用作实例方法(通过在对象上调用该方法隐式绑定 self 参数,例如 S().f,或者通过显式绑定它,例如 S.f(S()))时,您将创建一个新的闭包对象。该对象存储指向 S.f(可以共享)的指针,但也存储了您的实例(self,即 S())。 - Alexander
1
这个闭包对象必须在每个 S 实例中是唯一的。如果闭包指针相等是可能的,那么你会惊讶地发现 s1.f 不是与 s2.f 相同的指针(因为一个是引用 s1f 的闭包对象,而另一个是引用 s2f 的闭包对象)。 - Alexander
太棒了,谢谢!是的,到现在为止我已经有一个大致的了解,这让一切都更加清晰了! - Ian Bytchek

3

这不是通用解决方案,但如果要实现监听器模式,我最终会在注册期间返回函数的“id”,以便稍后用它注销(这是一种对原始问题“listeners”情况的变通方法,因为通常来说注销最终会涉及检查函数是否相等,这至少不像其他答案所说的那样“简单”)。

大致如下:

class OfflineManager {
    var networkChangedListeners = [String:((Bool) -> Void)]()

    func registerOnNetworkAvailabilityChangedListener(_ listener: @escaping ((Bool) -> Void)) -> String{
        let listenerId = UUID().uuidString;
        networkChangedListeners[listenerId] = listener;
        return listenerId;
    }
    func unregisterOnNetworkAvailabilityChangedListener(_ listenerId: String){
        networkChangedListeners.removeValue(forKey: listenerId);
    }
}

现在你只需要存储“register”函数返回的key,并在注销时传递它即可。

感谢您的出色回答!这似乎是一种最简单的方法来破解 Swift 无法比较函数引用的问题。我已经实现了一个简单的属性 private var listenerId = 0,并在重新注册监听器时递增它并返回,以避免使用复杂的 UUID().uuidString - mikep

2

两天过去了,没有人提供解决方案,所以我将我的评论改为答案:

据我所知,您无法检查函数(例如您的示例)和元类(例如 MyClass.self)的相等性或标识:

但是 - 这只是一个想法 - 我不禁注意到泛型中的where 从句似乎能够检查类型的相等性。因此,您可能可以利用它,至少用于检查标识?


0

你可以使用callAsFunction方法,例如:

struct MyType: Equatable {
    func callAsFunction() {
        print("Image a function")
    }

    static func == (lhs: MyType, rhs: MyType) -> Bool { true }
}

let a = MyType()
let b = MyType()
a()
b()
let e = a == b

在这种情况下,它们总是为真的,您可以使用初始化程序t给它们不同的内部状态,或其他方法来改变它们的状态,并且callAsFunction可以更改为接受参数。
不确定为什么===在实际函数上不起作用,因为您只是测试地址,但==调用Equatable协议的==方法,而函数不实现此协议。

0
我的解决方案是将函数封装到扩展NSObject的类中。
class Function<Type>: NSObject {
    let value: (Type) -> Void

    init(_ function: @escaping (Type) -> Void) {
        value = function
    }
}

当你这样做时,如何比较它们?假设你想从包装器数组中删除其中一个,你该怎么做?谢谢。 - Ricardo

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