我想深入了解Swift中的方法调度。我从this popular blog上读到了三种类型的调度,如下所示:
在该博客中,作者说NSObject子类型维护一个调度表(见证表)以及一个消息调度层次结构。作者共享的代码段如下:
class Person: NSObject {
func sayHi() {
print("Hello")
}
}
func greetings(person: Person) {
person.sayHi()
}
greetings(person: Person()) // prints 'Hello'
class MisunderstoodPerson: Person {}
extension MisunderstoodPerson {
override func sayHi() {
print("No one gets me.")
}
}
greetings(person: MisunderstoodPerson()) // prints 'Hello'
我将引用作者在Person实例上调用sayHi()的推理:
问候(person :)方法使用表调度来调用sayHi()。这将按预期结算,并打印“Hello”。没有什么太令人兴奋的了。现在,让我们继承Person
作者继续解释对MisunderstoodPerson对人物进行类型转换的实例的调用sayHi():
请注意,sayHi()在扩展中声明,这意味着将使用消息调度来调用该方法。当调用问候(person :)时,sayHi()通过表调度被分派到Person对象。由于MisunderstoodPerson覆盖是通过消息调度添加的,因此MisunderstoodPerson的调度表仍然在调度表中具有Person实现,并且随之产生混淆。
我想知道作者如何得出结论,问候(person :)方法使用表调度来调用sayHi()
作者之前在博客中提到的一件事是当NSObject子类在初始声明中声明一个方法(意思是不在扩展中)时,将使用表分派。
所以我假设参数'person'类型是问候(person :)方法的Person,并且调用的方法是sayHi(),它在Person类的初始声明中声明,使用表调度和sayHi()来自人被召唤。可以说使用人见证表是安全的。
一旦我们有子类MisunderstoodPerson并将此实例传递给问候(人:),这个实例应该被视为Person。我在这里有一个困惑,这里有几个问题。
作者没有在博客中澄清这应该使用谁的见证表。即使在某些情况下,我也会觉得作者甚至描述了为协议创建见证表的编译器。从他在博客中分享的图像可以看出,如下所示。
那篇博文有点过时了,继承自NSObject
不再改变一个类的调度行为(在Swift 3中,它会导致成员被隐式暴露给Obj-C,这将改变扩展成员的调度行为,but this is no longer the case) 。他们提供的示例也不再在Swift 5中编译,因为您只能覆盖扩展中的dynamic
成员。
为了区分静态调度和动态调度,让我们分别考虑协议。对于协议,如果两者都使用动态分派:
P
,协议组合类型值P & X
或通用占位符类型值T : P
上调用,例如:
protocol P {
func foo()
}
struct S : P {
func foo() {}
}
func bar(_ x: S) {
x.foo() // Statically dispatched.
}
func baz(_ x: P) {
x.foo() // Dynamically dispatched.
}
func qux<T : P>(_ x: T) {
x.foo() // Also dynamically dispatched.
}
如果协议是@objc
,则使用消息调度,否则使用表调度。
对于非协议成员,您可以问一个问题:“这可以覆盖吗?”。如果答案是否定的,那么您正在查看静态调度(例如,struct
成员或final
类成员)。如果它可以被覆盖,那么你正在寻找某种形式的动态调度。但是值得注意的是,如果优化器可以证明它没有被覆盖(例如,如果它是fileprivate
而不是在该文件中覆盖),则可以优化它以使用静态分派。
对于普通的方法调用,dynamic
修饰符区分了当前形式的动态调度,表分派和Obj-C消息调度。如果成员是dynamic
,Swift将使用消息调度。如上所述,这条规则似乎非常简单,但是有些成员没有明确标记为dynamic
- 编译器推断它。这包括:
dynamic
成员的覆盖。NSManaged
属性。@objc
class extension members。Swift中一种鲜为人知的方法调用形式是动态方法调用,它通过访问@objc
值上的AnyObject
成员来完成。例如:
import Foundation
class C {
@objc func foo() {}
}
func bar(_ x: AnyObject) {
// Message dispatch (crashing if the object doesn't respond to foo:).
x.foo!()
}
此类调用始终使用消息调度。
我认为这总结了当前使用调度机制的规则。
一旦我们有子类
MisunderstoodPerson
并将此实例传递给greetings(person:)
,此实例应被视为Person
。我在这里有一个困惑,这里有几个问题。
- 该实例的类型为
MisunderstoodPerson
,因此将在此处使用MisunderstoodPerson
的见证表。- 或者实例已被强制转换为
Person
,因此将在这里使用Person
的见证表。
(轻微术语nitpick:对于类,它被称为vtable而不是见证表)
它始终是vtable,它对应于所使用的实例的动态类型,所以在这种情况下它将是MisunderstoodPerson
的vtable。