提问人:Alex 提问时间:10/17/2023 最后编辑:AlexanderAlex 更新时间:10/17/2023 访问量:36
Swift 方法调度很奇怪:被覆盖的方法使用超类的默认参数值
Swift method dispatch is weird: Overridden method is using superclass' default parameter value
问:
为什么在操场上会有这么奇怪的结果? 如此诡异的调度机制是什么?
class A {
func execute(param: Int = 123) {
print("A: \(param)")
}
}
class B: A {
override func execute(param: Int = 456) {
print("B: \(param)")
}
}
let instance: A = B()
instance.execute()
// print B: 123
我看了SIL文件,但看起来还不错。 Vtables 看起来也不错。
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @riddle.instance : riddle.A // id: %2
%3 = global_addr @riddle.instance : riddle.A : $*A // users: %9, %8
%4 = metatype $@thick B.Type // user: %6
// function_ref B.__allocating_init()
%5 = function_ref @riddle.B.__allocating_init() -> riddle.B : $@convention(method) (@thick B.Type) -> @owned B // user: %6
%6 = apply %5(%4) : $@convention(method) (@thick B.Type) -> @owned B // user: %7
%7 = upcast %6 : $B to $A // user: %8
store %7 to %3 : $*A // id: %8
%9 = load %3 : $*A // users: %12, %13
// function_ref default argument 0 of A.execute(param:)
%10 = function_ref @default argument 0 of riddle.A.execute(param: Swift.Int) -> () : $@convention(thin) () -> Int // user: %11
%11 = apply %10() : $@convention(thin) () -> Int // user: %13
%12 = class_method %9 : $A, #A.execute : (A) -> (Int) -> (), $@convention(method) (Int, @guaranteed A) -> () // user: %13
%13 = apply %12(%11, %9) : $@convention(method) (Int, @guaranteed A) -> ()
%14 = integer_literal $Builtin.Int32, 0 // user: %15
%15 = struct $Int32 (%14 : $Builtin.Int32) // user: %16
return %15 : $Int32 // id: %16
} // end sil function 'main'
为什么行为如此奇怪?
答:
这是一种非常古老、有争议的行为。简短的回答是:不要那样做。关于这是一个错误还是一个设计选择,存在相当多的争论。
我个人的看法是,这应该是一个编译器错误,或者至少是一个警告。IMO,B 中的函数是不同的函数,因此不会覆盖 A(所以应该是一个错误)。或者它是“相同”的功能,但以某种方式具有不同的签名,这也应该是一个错误。至少有一个开放的错误可以使其成为警告(嗯,类似的东西,但它在同一个球场上)。override
但事实就是如此,我怀疑改变它是否会有太大的兴趣。这将是一个重大变化,所以无论如何我都不会期望它在 Swift 6 之前出现。由于继承不是 Swift 中的首选方法,因此大多数新功能都集中在使用结构、枚举或参与者的更首选方法上。所以我真的不指望在 Swift 6 中出现它。当然,如果它对你很重要,我会在论坛上提出来,并做一个推销。如果您感兴趣,至少发出警告可能是一个非常好的入门错误。
与大多数涉及默认值的问题一样,解决方案始终是记住它们实际上是更明确的语法的简写:
class A {
func execute() {
execute(param: 123)
}
func execute(param: Int) {
print("A: \(param)")
}
}
class B: A {
override func execute() {
execute(param: 456)
}
override func execute(param: Int) {
print("B: \(param)")
}
}
或者,您可以使用我一直认为对默认参数有用的技术。将它们指定为 Optional,并在实现中分配其默认值:
class A {
func execute(param: Int? = nil) {
let param = param ?? 123
print("A: \(param)")
}
}
class B: A {
override func execute(param: Int? = nil) {
let param = param ?? 456
print("B: \(param)")
}
}
这是我一直在发现的一种技术,如果这些默认值可能会访问属性,那么我经常需要使用默认值的方法。它也适用于这种情况。actor
@MainActor
(但你感到惊讶是对的。多年来,人们一直对此感到惊讶。
你在评论中问了到底发生了什么,这也是一个好问题。正如您所料,答案在 SIL 中(添加了评论):
# ...
# Take the B just created in %6 and upcast it to A; store in %9
%7 = upcast %6 : $B to $A // user: %8
store %7 to %3 : $*A // id: %8
%9 = load %3 : $*A // users: %12, %13
# Look up the default parameter A.execute(param:), since %9 is of type A
// function_ref default argument 0 of A.execute(param:)
%10 = function_ref @$s1x1AC7execute5paramySi_tFfA_ : $@convention(thin) () -> Int // user: %11
%11 = apply %10() : $@convention(thin) () -> Int // user: %13
# And apply the function `execute` to %9
%12 = class_method %9 : $A, #A.execute : (A) -> (Int) -> (), $@convention(method) (Int, @guaranteed A) -> () // user: %13
%13 = apply %12(%11, %9) : $@convention(method) (Int, @guaranteed A) -> ()
默认参数实际上是一个函数:
// default argument 0 of A.execute(param:)
sil hidden @$s1x1AC7execute5paramySi_tFfA_ : $@convention(thin) () -> Int {
bb0:
%0 = integer_literal $Builtin.Int64, 123 // user: %1
%1 = struct $Int (%0 : $Builtin.Int64) // user: %2
return %1 : $Int // id: %2
} // end sil function '$s1x1AC7execute5paramySi_tFfA_'
因此,由于是静态的类型(这是编译器所知道的),因此代码被转换为:instance
A
instance.execute()
func defaultArg0OfAExecuteParam() { 123 }
// ...
let param = defaultArg0OfAExecuteParam()
instance.execute(param: param)
所有选择都是在调用方的点上做出的,而不是在实现点上做出的。因此,默认值完全取决于静态已知的类型信息。
按照你所期望的方式工作将是 Swift 工作方式的重大变化,我不认为这是合理的,甚至不是特别可取的,因为它可能会减少内联和其他优化的机会。但是,IMO,允许这种情况的事实是一个错误。
评论