如何将实例方法用作仅接受函数或字面闭包的函数的回调函数

37

我正在"ViewController.swift"中创建此回调函数:

func callback(cf:CFNotificationCenter!, 
    ump:UnsafeMutablePointer<Void>, 
    cfs:CFString!, 
    up:UnsafePointer<Void>, 
    cfd:CFDictionary!) -> Void {

}

使用此观察者:

CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), 
    nil, 
    self.callback, 
    "myMESSage", 
    nil, 
    CFNotificationSuspensionBehavior.DeliverImmediately)

编译器错误的结果如下:

“C函数指针只能由对'func'的引用或文本闭包组成”


嗨,你解决了吗? - elect
@elect,你只需要将它从你的视图控制器类中移出来。它需要成为全局变量。 - Leo Dabus
2个回答

53
回调函数是指向C函数的指针,在Swift中,您只能传递全局函数或不捕获任何状态的闭包(closure),而不能传递实例方法。
因此,下面的代码可行:
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
        nil,
        { (_, observer, name, _, _) in
            print("received notification: \(name)")
        },
        "myMessage",
        nil,
        .DeliverImmediately)

但由于闭包无法捕获上下文,您无法直接引用self及其属性和实例方法。 例如,您无法添加

self.label.stringValue = "got it"
// error: a C function pointer cannot be formed from a closure that captures context

当通知到达时,在闭包内更新界面是有解决方案的。

但是由于Swift的严格类型系统,这个解决方案有点复杂。 与Swift 2 - UnsafeMutablePointer<Void> to object类似, 你可以将self转换为一个void指针,将其作为observer参数传递给注册函数,并在回调函数中将其转换回对象指针。

class YourClass { 

    func callback(name : String) {
        print("received notification: \(name)")
    }

    func registerObserver() {

        // Void pointer to `self`:
        let observer = UnsafePointer<Void>(Unmanaged.passUnretained(self).toOpaque())

        CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
            observer,
            { (_, observer, name, _, _) -> Void in

                // Extract pointer to `self` from void pointer:
                let mySelf = Unmanaged<YourClass>.fromOpaque(
                        COpaquePointer(observer)).takeUnretainedValue()
                // Call instance method:
                mySelf.callback(name as String)
            },
            "myMessage",
            nil,
            .DeliverImmediately)
    }

    // ...
}

闭包作为实例方法的“跳板”。

该指针是非持有引用,因此必须确保在对象被释放之前移除观察器。


Swift 3更新:

class YourClass {

    func callback(_ name : String) {
        print("received notification: \(name)")
    }

    func registerObserver() {

        // Void pointer to `self`:
        let observer = UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque())

        CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
            observer,
            { (_, observer, name, _, _) -> Void in
                if let observer = observer, let name = name {

                    // Extract pointer to `self` from void pointer:
                    let mySelf = Unmanaged<YourClass>.fromOpaque(observer).takeUnretainedValue()
                    // Call instance method:
                    mySelf.callback(name.rawValue as String)
                }
            },
            "myMessage" as CFString,
            nil,
            .deliverImmediately)
    }

    // ...
}
另请参见如何在Swift中将self强制转换为UnsafeMutablePointer<Void>类型,以了解有关对象指针和C指针之间的“桥接”的更多信息。

我使用了你的方法,但仍然出现错误(“无法从捕获上下文的闭包中形成C函数指针”)。但是当尝试使用mySelf调用实例方法时,Xcode没有抱怨,它可以看到该方法。 - nsinvocation
嗨@MartinR,感谢您的解决方案。但我认为您在答案中的语句“闭包无法捕获上下文”是不正确的。闭包可以捕获上下文,但函数海报或C函数不能。如果我误解了什么,请告诉我。 - Saurav Nagpal
2
@SauravNagpal:只有全局函数或不捕获上下文的闭包才能作为函数指针参数传递给C函数。这就是我在第一句话中试图表达的内容。 - Martin R
如果您需要针对CFNotificationCenterGetDistributedCenter并且目标操作系统为OS X 10.10或更高版本的解决方案,您也可以使用NSDistributedNotificationCenter,它允许您传递选择器并具有更好的API。 - Felix
你刚刚将我的问题标记为与此重复,但我们的要求完全不同。这个问题是否应该包含ops需求的答案?您能否在我的答案中添加解决方案的评论? - Hemang
@Hemang:这正是相同的问题(您不能在闭包中使用“self”),并且正好有相同的解决方案:您必须将self转换为void指针,将其作为上下文指针传递给回调函数(在您的情况下使用CGPDFScannerCreate的最后一个参数),并在回调函数中将其转换回实例指针。- 这里有另一个使用相同技术的示例:https://dev59.com/M10a5IYBdhLWcg3wCE0w#30788165。 - Martin R

3

在我的情况下,我想从我的闭包中调用的函数位于AppDelegate中。因此,我能够使用委托来从闭包中调用函数而不使用self。这是否是一个好主意需要有更多经验的人来评论。

        self.pingSocket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP,CFSocketCallBackType.dataCallBack.rawValue, {socket, type, address, data, info in
            //type is CFSocketCallBackType
            guard let socket = socket, let address = address, let data = data, let info = info else { return }

// stuff deleted, including use of C pointers

            let appDelegate = NSApplication.shared.delegate as! AppDelegate
            appDelegate.receivedPing(ip: sourceIP, sequence: sequence, id: id)
            //}
            return
        }, &context)

extension AppDelegate: ReceivedPingDelegate {
    func receivedPing(ip: UInt32, sequence: UInt16, id: UInt16) {
        // stuff deleted
    }
}
protocol ReceivedPingDelegate: class {
    func receivedPing(ip: UInt32, sequence: UInt16, id: UInt16)
}

3
对我来说行不通(Swift 5,Xcode 10.2):无法从捕获上下文的本地函数形成C函数指针 - ixany

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