swift 协议扩展的调用层次结构问题

Issue with the call hierarchy of a swift protocol extension

我已经为 Alamofire 的可达性实现了一个小包装器。现在我 运行 遇到一个子 class 没有收到通知的问题。

正如 dfri 在评论中添加的那样,问题可以描述为 a superclass subclass 实例调用的方法没有看到被覆盖的 subclass 方法,如果它被放置在扩展.

感谢他出色的要点,展示了通常 foo/bar 的可重现问题:https://gist.github.com/dfrib/01e4bf1020e2e39dbe9cfb14f4165656

协议&扩展逻辑:

protocol ReachabilityProtocol: class {    
    func reachabilityDidChange(online: Bool)
}

extension ReachabilityProtocol {

    func configureReachabilityManager() {
        // triggered externally:
        rechabilityManager?.listener = { [weak self] status in
            self?.reachabilityDidChange(online)
        }
    }

    func reachabilityDidChange(online: Bool) {
        // abstract, implement in subclasses
    }

}

现ViewControllerA采用此协议并实现方法:

class A: UIViewController, ReachabilityProtocol {

    func reachabilityDidChange(online: Bool) {
        debugPrint("123")
    }

}

工作到这里,打印 123

ViewController B 是一个 subclass 并覆盖 A:

class B: A {

    override func reachabilityDidChange(online: Bool) {
        super.reachabilityDidChange(online)
        debugPrint("234")
    }

}

也在工作。现在打印 234123 收到通知。

现在是棘手的部分,在构建代码方面,重写的方法被移动到 B 中的一个自己的扩展中(与 swift 文件相同71=] B):

class B: A {
...
}

// MARK: - Reachability Notification
extension B {

    override func reachabilityDidChange(online: Bool) {
        super.reachabilityDidChange(online)
        debugPrint("234")
    }

}

现在 BreachabilityDidChange() 不再被调用。输出只是 123

为了检查逻辑,我什至尝试删除 override 关键字:编译器立即抱怨需要它。

据我所知,调用层次结构是正确的。也能见度。

归纳:如果重写的方法放在它自己的扩展中,它不再是visible/invoked,但编译器仍然需要override关键字。

这是我还没有遇到的事情,或者是我今天在同一个项目上工作了很长时间..

有什么提示吗?

覆盖扩展中的超级class方法:只允许Objective-C兼容方法

首先我们注意到,如果该方法与 Objective-C 兼容,则您只能在子class 的扩展中重写 superclass 方法。对于从 NSObject 派生的 classes,这对所有实例方法都是正确的(这里是正确的,因为 UIViewController 是从 NSObject 派生的)。这包括在例如以下问答:

  • Can you override between extensions in Swift or not? (Compiler seems confused!)

通过Objective-C 运行时

强制执行动态调度

现在,根据 Interoperability - Interacting with Objective-C APIs - Requiring Dynamic Dispatch,我们注意到以下内容

When Swift APIs are imported by the Objective-C runtime, there are no guarantees of dynamic dispatch for properties, methods, subscripts, or initializers. The Swift compiler may still devirtualize or inline member access to optimize the performance of your code, bypassing the Objective-C runtime.

You can use the dynamic modifier to require that access to members be dynamically dispatched through the Objective-C runtime.

此外,从The Language Ref. - Declarations - Declaration Modifyers我们读到

dynamic (modifier)

Apply this modifier to any member of a class that can be represented by Objective-C. When you mark a member declaration with the dynamic modifier, access to that member is always dynamically dispatched using the Objective-C runtime. Access to that member is never inlined or devirtualized by the compiler.

Because declarations marked with the dynamic modifier are dispatched using the Objective-C runtime, they’re implicitly marked with the objc attribute.

A

中为 reachabilityDidChange(...) 强制执行动态调度

因此,如果您将 dynamic 修饰符添加到 superclass A 中的方法 reachabilityDidChange(...),那么对 reachabilityDidChange(...) 的访问将始终是动态的使用 Objective-C 运行时调度,因此在 class B 扩展中找到并使用正确的覆盖 reachabilityDidChange(...) 方法,用于 B 的实例。因此,

dynamic func reachabilityDidChange(online: Bool) { ... } 

A 中将修复上述问题。


下面是您上述问题的一个更简单的示例,通过 obj-c 运行时要求对 class A 中的方法 foo() 进行动态调度(相当于您的方法 reachabilityDidChange(...) 在 class A)。

import UIKit

protocol Foo: class {
    func foo()
}

extension Foo {
    func foo() { print("default") }
}

class A: UIViewController, Foo {
    dynamic func foo() { print("A") } // <-- note dynamic here
    func bar() { self.foo() }  
            /*          \
                   hence, foo() is dynamically dispatched here, and for
                   instances where "self === B()", this is correctly
                   resolved as the foo() in the extension to B          */
}

class B : A { }

extension B {
    override func foo() {
        super.foo()
        print("B")
    }
}

let a = A()
a.bar() // A

let b = B()
b.bar() // A B
        /* or, only "A" if not using dynamic dispatch */