应该使用谁的见证 table?

Whose witness table should be used?

我想深入了解 Swift 中的方法调度。我从 this popular blog 中了解到以下三种类型的调度:

  1. 动态
  2. Table(在Swift中见证table)
  3. 留言

在那篇博客中,作者说 NSObject 子类型维护一个调度 table(见证 table)以及一个消息调度层次结构。 作者分享的代码片段如下:

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() 的推理:

The greetings(person:) method uses table dispatch to invoke sayHi(). This resolves as expected, and “Hello” is printed. Nothing too exciting here. Now, let’s subclass Person

作者继续解释了对 MisunderstoodPerson 类型转换为 Person:

的实例的调用 sayHi()

Notice that sayHi() is declared in an extension meaning that the method will be invoked with message dispatch. When greetings(person:) is invoked, sayHi()is dispatched to the Person object via table dispatch. Since the MisunderstoodPerson override was added via message dispatch, the dispatch table for MisunderstoodPerson still has the Person implementation in the dispatch table, and confusion ensues.

我想知道作者是如何得出结论的 greetings(person:) 方法使用 table dispatch 来调用 sayHi()

作者之前在博客中提到的一件事是,当 NSObject subclass 在初始声明中声明一个方法时(意味着不在扩展中),将使用 table 调度。

所以我假设参数 'person' 类型是 Person 对于 greetings(person:) 方法和调用的方法是 sayHi(),它在 Personclassatable 的初始声明中声明,使用分派和 sayHi()人叫可以肯定地说,使用了 Person witness table

一旦我们有了 subclass MisunderstoodPerson 并将这个实例传递给 greetings(person:) 这个实例应该被视为 Person. 我在这里很困惑,在这里有几个问题。

  • 实例的类型是 MisunderstoodPerson 所以会见证 这里使用 MisunderstoodPersontable。
  • 或者实例已被类型转换为 Person,因此此处将使用 Person 的 witness table。

作者在博客中没有明确说明应该使用谁的见证table。甚至在某些情况下,我感觉作者甚至描述了编译器为协议创建 witness tables。从他在博客中分享的图片可以看出,如下所示。

如果有人能解释一下,我将不胜感激。

那个博客 post 有点过时了,因为从 NSObject 继承不再更改 class 的调度行为(在 Swift 3 中,它会导致成员隐式暴露给 Obj-C,这将改变扩展成员的调度行为,)。他们给出的示例也不再在 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,则使用消息调度,否则使用 table 调度。

对于非协议成员,您可以提出问题:"Can this be overriden?"。如果答案是否定的,那么您正在查看静态调度(例如 struct 成员或 final class 成员)。如果它可以被覆盖,那么您正在寻找某种形式的动态调度。然而,值得注意的是,如果优化器可以证明它没有被覆盖(例如,如果它是 fileprivate 并且在该文件中没有被覆盖),那么它可以被优化为使用静态调度。

对于普通的方法调用,dynamic 修饰符区分当前两种形式的动态调度,table 调度和 Obj-C 消息调度。如果成员是 dynamic,Swift 将使用消息调度。如前所述,这条规则看起来非常简单,但是一些成员没有明确标记为 dynamic——编译器会推断它。这包括:

  • 从 Objective-C.
  • 导入的成员
  • 覆盖 dynamic 个成员。
  • NSManaged 个属性。
  • .

Swift 中一种鲜为人知的方法调用形式是动态方法调用,它是通过访问 AnyObject 值上的 @objc 成员来完成的。例如:

import Foundation

class C {
  @objc func foo() {}
}

func bar(_ x: AnyObject) {
  // Message dispatch (crashing if the object doesn't respond to foo:).
  x.foo!()
}

此类调用始终使用消息调度。

我认为这总结了当前使用哪些调度机制的规则。


Once we have subclass MisunderstoodPerson and pass this instance to greetings(person:) this instance should be treated as Person. I have a confusion here and have couple of questions here.

  • The instance is of type MisunderstoodPerson so would witness table of MisunderstoodPerson be used here.
  • Or the instance has been typecast to Person, so would witness table of Person be used here.

(有点吹毛求疵的术语:对于classes,它被称为vtable而不是witness table)

始终是 vtable 对应于所使用实例的动态类型,因此在本例中它将是 MisunderstoodPerson 的 vtable.