您如何测试函数和闭包是否相等?


88

该书说“函数和闭包是引用类型”。那么,如何确定引用是否相等?==和===不起作用。

func a() { }
let å = a
let b = å === å // Could not find an overload for === that accepts the supplied arguments

5
据我所知,您也无法检查元类的相等性(例如MyClass.self
Jiaaro

不必比较两个闭包的身份。您能举个例子说明如何做吗?可能有替代解决方案。
比尔

1
组播闭包,la C#。在Swift中,它们一定比较丑陋,因为您不能重载(T,U)“运算符”,但是我们仍然可以自己创建它们。但是,由于无法通过引用从调用列表中删除闭包,我们需要创建自己的包装器类。这是一个阻力,并且不是必须的。
Jessy 2014年

2
很好的问题,但又是完全分开的问题:您使用变音符号å进行引用a确实很有趣。您在这里探索一个约定吗?(我不知道我是否真的喜欢它;但是看起来它可能非常强大,尤其是在纯函数式编程中。)
Rob Napier 2014年

2
我@Bill我存储在数组关闭和,以便找到并删除它们无法使用的indexOf({$ 0 ==关闭}现在,我必须调整自己的代码由于其优化我认为是语言设计不良。
Zack Morris

Answers:


72

克里斯·拉特纳(Chris Lattner)在开发人员论坛上写道:

这是我们有意不想支持的功能。有多种情况会导致函数的指针相等(在快速类型的系统意义上,其中包括几种闭包)根据优化而失败或更改。如果在函数上定义了“ ===”,则不允许编译器合并相同的方法主体,共享thunk并在闭包中执行某些捕获优化。此外,在某些泛型上下文中,此类相等将是非常令人惊讶的,在这种情况下,您会得到重新提取的重击,将函数的实际签名调整为该函数类型所期望的签名。

https://devforums.apple.com/message/1035180#1035180

这意味着您甚至不应该尝试比较闭包是否相等,因为优化可能会影响结果。


18
这只是让我有些吃惊,因为我一直将闭包存储在Array中,现在无法使用indexOf({$ 0 == Closure}删除它们,所以我必须重构。恕我直言,优化不应影响语言设计,因此,如果没有像matt的答案中现在已弃用的@objc_block这样的快速修复方法,我会认为Swift无法在此时正确存储和检索闭包,因此我认为在回调沉重的代码中倡导使用Swift是不合适的就像Web开发中遇到的那种。这就是我们首先改用Swift的全部原因……
Zack Morris

4
@ZackMorris在闭包中存储某种标识符,以便稍后将其删除。如果使用引用类型,则只能存储对对象的引用,否则可以提出自己的标识符系统。您甚至可以设计一个具有闭包和唯一标识符的类型,以代替普通的闭包。
drewag '16

5
@drewag是的,有解决方法,但是Zack是正确的。这真的很la脚。我知道想要进行优化,但是如果开发人员需要在代码中某个地方比较某些闭包,那么只需让编译器不优化那些特定的部分即可。或者,使编译器具有某种附加功能,使其能够创建不会因异常优化而中断的相等性签名。这就是我们在这里谈论的Apple ...如果他们可以将Xeon装入iMac,那么他们当然可以使闭包具有可比性。休息一下
CommaToast

10

我搜了很多。似乎没有函数指针比较的方法。我得到的最佳解决方案是将函数或闭包封装在可哈希对象中。喜欢:

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

2
到目前为止,这是最好的方法。必须包装和解开闭包很烂,但是比不确定的,不受支持的脆弱性要好。

8

最简单的方法是将块类型指定为@objc_block,现在您可以将其转换为与相似的AnyObject ===。例:

    typealias Ftype = @objc_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

嘿,我正在尝试unsafeBitCast(listener,AnyObject.self)=== unsafeBitCast(f,AnyObject.self)但出现致命错误:不同大小类型之间不能unsafeBitCast。这个想法是建立一个基于事件的系统,但是removeEventListener方法应该能够检查函数指针。
freezing_

2
在Swift 2.x上使用@convention(block)而不是@objc_block。好答案!
Gabriel.Massana

6

我也一直在寻找答案。我终于找到了。

您需要的是实际的函数指针及其在函数对象中隐藏的上下文。

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

请参阅下面的URL,以了解其原因和工作方式:

如您所见,它仅能检查身份(第二次测试会产生false)。但这应该足够了。



8
这是基于未定义实现细节的黑客。然后使用这意味着您的程序将产生未定义的结果。
2014年

8
请注意,这依赖于未记录的内容和未公开的实现细节,这些细节将来可能会使您的应用崩溃。不建议在生产代码中使用。
Cristik '16

这是“三叶草”,但完全不可行。我不知道为什么会得到赏金。该语言故意不具有函数相等性,确切的目的是释放编译器以自由破坏函数相等性以产生更好的优化。
亚历山大-恢复莫妮卡

...而这恰恰是克里斯·拉特纳(Chris Lattner)提倡的方法(请参见最高答案)。
猪仔

4

这是一个很大的问题,尽管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为何不(也不应该)支持此理由。“我也觉得这种方法可能不好,如果有的话,有人能解释为什么吗?” 因为在优化的构建中,编译器可以通过多种方式自由地处理代码,从而打破了函数的点均等的思想。对于一个基本示例,如果一个函数的主体以与另一个函数相同的方式启动,则编译器可能会在机器代码中将二者重叠,仅保留不同的退出点。这减少了重复
亚历山大-恢复莫妮卡

1
从根本上讲,闭包是初始化匿名类对象的方法(就像在Java中一样,但是更明显)。这些闭包对象是堆分配的,并存储闭包捕获的数据,其作用类似于闭包函数的隐式参数。闭包对象持有对函数的引用,该函数对显式(通过func args)和隐式(通过捕获的闭包上下文)arg进行操作。尽管可以将函数体共享为一个唯一的点,但闭包对象的指针却不能,因为每组封闭值有一个闭包对象。
亚历山大-恢复莫妮卡

1
因此,当您拥有时Struct S { func f(_: Int) -> Bool },实际上就S.f具有具有type的type函数(S) -> (Int) -> Bool可以共享此功能。它仅通过其显式参数进行参数化。通过将其用作实例方法(通过self在对象上调用方法来隐式绑定参数,例如S().f,或通过显式绑定它(例如S.f(S()))),可以创建一个新的关闭对象。该对象存储一个指向S.f(可以共享的), but also to your instance (self , the S()`的指针。
亚历山大-恢复莫妮卡

1
每个的闭合对象必须是唯一的S。如果可以使用闭包指针相等,那么您会惊讶地发现它s1.f与指针不同s2.f(因为一个是引用s1f的闭包对象,另一个是引用s2和的闭包对象f)。
亚历山大-恢复莫妮卡

太好了,谢谢!是的,到目前为止,我对正在发生的事情有一个了解,这使所有事情都成为一个视角!👍–
伊恩·

4

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

迅速:

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

如果您将其设为通用,那就更好了。
亚历山大-恢复莫妮卡

2

到目前为止已经有两天了,没有人提出解决方案,所以我将评论更改为答案:

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

但是-这仅仅是一个主意-我不禁注意到where泛型中的子句似乎能够检查类型的相等性。那么也许您可以利用它,至少用于检查身份?


2

这不是一种通用的解决方案,但是如果尝试实现侦听器模式,那么我最终会在注册过程中返回该函数的“ id”,以便以后可以使用它注销(这是原始问题的一种解决方法对于“侦听器”,通常取消注册归结为检查功能是否相等,根据其他答案,这至少不是“琐碎的”)。

所以像这样:

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

现在,您只需要存储key“注册”函数返回的值,并在注销时传递它。


0

我的解决方案是将函数包装到扩展NSObject的类中

class Function<Type>: NSObject {
    let value: (Type) -> Void

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

当您这样做时,如何比较它们?假设您要从包装器数组中删除其中一个,该怎么做?谢谢。
里卡多

0

我知道我要迟到六年才回答这个问题,但是我认为值得研究这个问题背后的动机。发问者评论:

但是,由于无法通过引用从调用列表中删除闭包,我们需要创建自己的包装器类。这是一个阻力,并且不是必须的。

所以我想发问者想维护一个回调列表,像这样:

class CallbackList {
    private var callbacks: [() -> ()] = []

    func call() {
        callbacks.forEach { $0() }
    }

    func addCallback(_ callback: @escaping () -> ()) {
        callbacks.append(callback)
    }

    func removeCallback(_ callback: @escaping () -> ()) {
        callbacks.removeAll(where: { $0 == callback })
    }
}

但是我们不能这样写removeCallback,因为==它不适用于函数。(也没有===。)

这是管理回调列表的另一种方法。从返回一个注册对象addCallback,并使用该注册对象删除回调。在2020年,我们可以使用合并的AnyCancellable注册。

修改后的API如下所示:

class CallbackList {
    private var callbacks: [NSObject: () -> ()] = [:]

    func call() {
        callbacks.values.forEach { $0() }
    }

    func addCallback(_ callback: @escaping () -> ()) -> AnyCancellable {
        let key = NSObject()
        callbacks[key] = callback
        return .init { self.callbacks.removeValue(forKey: key) }
    }
}

现在,添加回调时,无需保留回调即可传递给removeCallback以后。没有removeCallback办法。相反,您保存AnyCancellable,并调用其cancel方法删除回调。更好的是,如果将int存储AnyCancellable在instance属性中,则销毁实例后它将自动取消自身。


我们需要这样做的最常见原因是为发布者管理多个订户。结合可以解决所有这些问题。C#允许并且Swift不允许的是,找出两个闭包是否引用相同的命名函数。这也很有用,但很少使用。
Jessy
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.