Dynamic/runtime 在 Swift 或 "the strange way structs behave in one man's opinion" 中派遣

Dynamic/runtime dispatch in Swift, or "the strange way structs behave in one man's opinion"

我对 Swift 并不陌生,对 Objective-C 也不是很陌生,但我今天在使用 Error 子类型时看到了一些奇怪的行为,这让我开始深入研究更深。

在使用 NSString 子类时(是的,下面的示例基于 NSObjectnot 的功能类似):

import Foundation

// Class version
class OddString : NSString {
    override var description: String {
        return "No way, José"
    }
}

let odd = OddString()

func printIt(_ string: NSString) {
    print(string.description)
}

print(odd.description)
printIt(odd)

我看到了我期望看到的:

No way, José
No way, José

然而,当我编写(我认为是)等效代码时使用结构(Error)代替:

import Foundation

// Struct version
struct TestError : Error {
    var localizedDescription: String {
        return "I am a TestError"
    }
}

let explosive = TestError()

func printIt(_ error : Error) {
    print(error.localizedDescription)
}

print(explosive.localizedDescription)
printIt(explosive)

我明白了:

I am a TestError
The operation couldn’t be completed. (SanityChecks.TestError error 1.)

这让我很困惑。它是否在编译时决定在传递给printIt的结构上调用什么方法,而不管它实际上是什么类型?

进一步:类 和 Swift 编程指南中记录的结构之间的运行时行为差异是否存在,有人可以参考该部分吗?我还没有找到任何关于此的内容。

localizedDescription 既是 Error 协议的扩展,也是错误类型的 属性。当编译器知道您的类型实现了 属性 时,它会使用它。如果没有,它会使用扩展名。没有动态调度。

与您的 String 示例对比,在该示例中,通过覆盖 description 成员,编译器将对您的实现的引用放在 vtable 中,因此它将被动态调度。

在您的第一个示例中,您覆盖 description 属性。因此,这个实现被添加到 OddString 的 vtable(因为它是一个 class),并且可以动态调度到很好,不管实例是什么静态类型为.

在你的第二个例子中,你没有 class – 所以没有虚表。但是,您 遵守协议。协议允许通过协议见证表进行动态调度(参见 this great WWDC talk),但这仅发生在协议 requirements.

的实现中

localizedDescription 不是 Error 协议的协议要求,当您 import Foundation 时,它只是在 Error 的协议扩展中定义(这记录在SE-0112)。因此它不能被动态调度。相反,它将被静态调度——因此调用的实现取决于实例的 static 类型

这就是您在这里看到的行为 – 当您的 explosive 实例被键入为 TestError 时,您的 localizedDescription 的实现被调用。当键入 Error 时,将调用 Error 扩展中的实现(它只是桥接到 NSError 并获得它的 localizedDescription)。

如果您想提供本地化的描述,那么您应该将您的错误类型与 LocalizedError instead, which defines errorDescription as a protocol requirement – thus allowing for dynamically dispatch. See 一致,以获取有关如何执行此操作的示例。