【问题标题】:Whose witness table should be used?应该使用谁的见证表?
【发布时间】:2019-08-25 19:04:50
【问题描述】:

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

  1. 动态
  2. 表(Swift 中的见证表)
  3. 留言

在该博客中,作者说 NSObject 子类型维护一个调度表(见证表)以及一个消息调度层次结构。 作者分享的sn-p代码如下:

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

greetings(person:) 方法使用表调度来调用 sayHi()。 这会按预期解决,并打印“Hello”。也没什么 这里令人兴奋。现在,让我们继承 Person 类

作者继续解释了在 MisunderstoodPerson 类型转换为 Person 的实例上的调用 sayHi()

注意 sayHi() 是在扩展中声明的,这意味着 方法将通过消息调度调用。问候时(人:) 被调用,sayHi() 通过表被分派给 Person 对象 派遣。由于 MisunderstoodPerson 覆盖是通过消息添加的 调度,MisunderstoodPerson 的调度表仍然有 调度表中的人员实现,随之而来的是混乱。

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

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

所以我假设 greetings(person:) 方法的参数 'person' 类型是 Person 并且调用的方法是 sayHi() ,它在Person 类的初始声明使用了一个表调度,并调用了来自 Person 的 sayHi()可以肯定地说使用了 Person 见证表

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

  • 实例属于 MisunderstoodPerson 类型,因此会见证 MisunderstoodPerson 表在此处使用。
  • 或者该实例已被类型转换为 Person,因此此处将使用 Person 的见证表。

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

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

【问题讨论】:

    标签: swift dispatch


    【解决方案1】:

    那篇博文有点过时了,因为从 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——编译器会推断它。这包括:

    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!()
    }
    

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

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


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

    • 实例的类型为MisunderstoodPerson,因此见证表为 MisunderstoodPerson 在这里使用。
    • 或者实例已经被类型转换为Person,所以这里会使用Person的见证表。

    (轻微的术语挑剔:对于类,它被称为 vtable 而不是见证表)

    始终是对应于所使用实例的动态类型的 vtable,因此在这种情况下,它将是 MisunderstoodPerson 的 vtable。

    【讨论】:

    • 根据您的陈述,在协议扩展中声明的协议成员使用静态调度是否安全。还有你的意思是什么:成员在协议类型值 P 或通用占位符 T 上调用:P。你是说任何符合类型的实例,类型转换为 P?
    • 协议是否也维护 vtables?
    • @RohanBhale 通过T : P,我的意思是如果你有一个函数func foo&lt;T : P&gt;(_ x: T) { x.foo() }。如果foo()P 的协议要求,它将通过协议见证表动态调度。对于协议类型的P 值,是的,我的意思是一个符合类型的实例,强制转换为P
    • @RohanBhale 是的,会为每个一致性生成一个协议见证表——stackoverflow.com/a/44706021/2976878 可能有助于获得更多信息。
    • 好的。假设我的 MyClass 符合 MyProtocol。我有一个名为“x”的 MyClass 实例。您声明“始终是与所使用实例的动态类型相对应的 vtable。”假设 'x' 类型转换为 MyProtocol 引用保存在 MyProtocol 类型的 'y' 中。现在,如果我在“y”上调用方法,我可以肯定地说,即使在类型转换为 MyProtocol 之后,仍将使用 MyClass vtable 而不是 MyProtocol 的 vtable。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-06
    • 1970-01-01
    • 2014-12-05
    • 2011-04-13
    • 1970-01-01
    相关资源
    最近更新 更多