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
子类时(是的,下面的示例基于 NSObject
类 not 的功能类似):
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 一致,以获取有关如何执行此操作的示例。
我对 Swift 并不陌生,对 Objective-C 也不是很陌生,但我今天在使用 Error
子类型时看到了一些奇怪的行为,这让我开始深入研究更深。
在使用 NSString
子类时(是的,下面的示例基于 NSObject
类 not 的功能类似):
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