在 Swift 3 中从间接调用中调用了错误的专用泛型函数
Wrong specialized generic function gets called in Swift 3 from an indirect call
我的代码遵循以下一般设计:
protocol DispatchType {}
class DispatchType1: DispatchType {}
class DispatchType2: DispatchType {}
func doBar<D:DispatchType>(value:D) {
print("general function called")
}
func doBar(value:DispatchType1) {
print("DispatchType1 called")
}
func doBar(value:DispatchType2) {
print("DispatchType2 called")
}
实际上 DispatchType
实际上是一个后端存储。 doBar
函数是依赖于正确存储类型的优化方法。如果我这样做,一切正常:
let d1 = DispatchType1()
let d2 = DispatchType2()
doBar(value: d1) // "DispatchType1 called"
doBar(value: d2) // "DispatchType2 called"
但是,如果我创建一个调用 doBar
:
的函数
func test<D:DispatchType>(value:D) {
doBar(value: value)
}
我尝试了类似的调用模式,我得到:
test(value: d1) // "general function called"
test(value: d2) // "general function called"
这似乎是 Swift 应该能够处理的事情,因为它应该能够在编译时确定类型约束。作为快速测试,我还尝试将 doBar
写为:
func doBar<D:DispatchType>(value:D) where D:DispatchType1 {
print("DispatchType1 called")
}
func doBar<D:DispatchType>(value:D) where D:DispatchType2 {
print("DispatchType2 called")
}
但得到相同的结果。
如果这是正确的 Swift 行为,有什么想法吗?如果是,是否有解决此行为的好方法?
编辑 1:我试图避免使用协议的示例。假设我有代码(大大简化了我的实际代码):
protocol Storage {
// ...
}
class Tensor<S:Storage> {
// ...
}
对于 Tensor
class,我有一组可以在 Tensor
上执行的基本操作。然而,操作本身将根据存储改变它们的行为。目前我通过以下方式完成此操作:
func dot<S:Storage>(_ lhs:Tensor<S>, _ rhs:Tensor<S>) -> Tensor<S> { ... }
虽然我可以将它们放在 Tensor
class 中并使用扩展名:
extension Tensor where S:CBlasStorage {
func dot(_ tensor:Tensor<S>) -> Tensor<S> {
// ...
}
}
这有一些我不喜欢的副作用:
我认为 dot(lhs, rhs)
优于 lhs.dot(rhs)
。可以编写便利函数来解决这个问题,但这会造成代码量激增。
这将导致 Tensor
class 变得单一。我真的更喜欢让它包含最少的必要代码并通过辅助功能扩展其功能。
与(2)相关,这意味着任何人想要添加新功能都必须接触基础class,我认为这是糟糕的设计。
编辑 2:一种替代方法是,如果对所有内容都使用约束,事情就会按预期进行:
func test<D:DispatchType>(value:D) where D:DispatchType1 {
doBar(value: value)
}
func test<D:DispatchType>(value:D) where D:DispatchType2 {
doBar(value: value)
}
将导致调用正确的 doBar
。这也不理想,因为它会导致编写大量额外的代码,但至少让我保持当前的设计。
编辑 3:我看到了显示 static
关键字与泛型一起使用的文档。这至少有助于第 (1) 点:
class Tensor<S:Storage> {
// ...
static func cos(_ tensor:Tensor<S>) -> Tensor<S> {
// ...
}
}
允许你写:
let result = Tensor.cos(value)
并且支持运算符重载:
let result = value1 + value2
它确实比必需的 Tensor
更冗长。这可以做得更好一点:
typealias T<S:Storage> = Tensor<S>
这确实是正确的行为,因为重载解析发生在编译时(在运行时发生将是一个非常昂贵的操作)。因此,从 test(value:)
中,编译器对 value
的唯一了解是它是某种符合 DispatchType
的类型——因此 仅 重载它可以发送到 func doBar<D : DispatchType>(value: D)
.
如果泛型函数总是由编译器专门化,情况就会不同,因为 test(value:)
的专门实现会知道 value
的具体类型,从而能够选择适当的重载.然而,泛型函数的特化目前只是一种优化(因为没有内联,它会给你的代码增加显着的膨胀),所以这不会改变观察到的行为。
为了允许多态性,一个解决方案是通过添加 doBar()
作为协议要求来利用协议见证 table(参见 this great WWDC talk),并实现专门的它在符合协议的相应 类 中的实现,一般实现是协议扩展的一部分。
这将允许 doBar()
的动态调度,从而允许从 test(value:)
调用它并调用正确的实现。
protocol DispatchType {
func doBar()
}
extension DispatchType {
func doBar() {
print("general function called")
}
}
class DispatchType1: DispatchType {
func doBar() {
print("DispatchType1 called")
}
}
class DispatchType2: DispatchType {
func doBar() {
print("DispatchType2 called")
}
}
func test<D : DispatchType>(value: D) {
value.doBar()
}
let d1 = DispatchType1()
let d2 = DispatchType2()
test(value: d1) // "DispatchType1 called"
test(value: d2) // "DispatchType2 called"
我的代码遵循以下一般设计:
protocol DispatchType {}
class DispatchType1: DispatchType {}
class DispatchType2: DispatchType {}
func doBar<D:DispatchType>(value:D) {
print("general function called")
}
func doBar(value:DispatchType1) {
print("DispatchType1 called")
}
func doBar(value:DispatchType2) {
print("DispatchType2 called")
}
实际上 DispatchType
实际上是一个后端存储。 doBar
函数是依赖于正确存储类型的优化方法。如果我这样做,一切正常:
let d1 = DispatchType1()
let d2 = DispatchType2()
doBar(value: d1) // "DispatchType1 called"
doBar(value: d2) // "DispatchType2 called"
但是,如果我创建一个调用 doBar
:
func test<D:DispatchType>(value:D) {
doBar(value: value)
}
我尝试了类似的调用模式,我得到:
test(value: d1) // "general function called"
test(value: d2) // "general function called"
这似乎是 Swift 应该能够处理的事情,因为它应该能够在编译时确定类型约束。作为快速测试,我还尝试将 doBar
写为:
func doBar<D:DispatchType>(value:D) where D:DispatchType1 {
print("DispatchType1 called")
}
func doBar<D:DispatchType>(value:D) where D:DispatchType2 {
print("DispatchType2 called")
}
但得到相同的结果。
如果这是正确的 Swift 行为,有什么想法吗?如果是,是否有解决此行为的好方法?
编辑 1:我试图避免使用协议的示例。假设我有代码(大大简化了我的实际代码):
protocol Storage {
// ...
}
class Tensor<S:Storage> {
// ...
}
对于 Tensor
class,我有一组可以在 Tensor
上执行的基本操作。然而,操作本身将根据存储改变它们的行为。目前我通过以下方式完成此操作:
func dot<S:Storage>(_ lhs:Tensor<S>, _ rhs:Tensor<S>) -> Tensor<S> { ... }
虽然我可以将它们放在 Tensor
class 中并使用扩展名:
extension Tensor where S:CBlasStorage {
func dot(_ tensor:Tensor<S>) -> Tensor<S> {
// ...
}
}
这有一些我不喜欢的副作用:
我认为
dot(lhs, rhs)
优于lhs.dot(rhs)
。可以编写便利函数来解决这个问题,但这会造成代码量激增。这将导致
Tensor
class 变得单一。我真的更喜欢让它包含最少的必要代码并通过辅助功能扩展其功能。与(2)相关,这意味着任何人想要添加新功能都必须接触基础class,我认为这是糟糕的设计。
编辑 2:一种替代方法是,如果对所有内容都使用约束,事情就会按预期进行:
func test<D:DispatchType>(value:D) where D:DispatchType1 {
doBar(value: value)
}
func test<D:DispatchType>(value:D) where D:DispatchType2 {
doBar(value: value)
}
将导致调用正确的 doBar
。这也不理想,因为它会导致编写大量额外的代码,但至少让我保持当前的设计。
编辑 3:我看到了显示 static
关键字与泛型一起使用的文档。这至少有助于第 (1) 点:
class Tensor<S:Storage> {
// ...
static func cos(_ tensor:Tensor<S>) -> Tensor<S> {
// ...
}
}
允许你写:
let result = Tensor.cos(value)
并且支持运算符重载:
let result = value1 + value2
它确实比必需的 Tensor
更冗长。这可以做得更好一点:
typealias T<S:Storage> = Tensor<S>
这确实是正确的行为,因为重载解析发生在编译时(在运行时发生将是一个非常昂贵的操作)。因此,从 test(value:)
中,编译器对 value
的唯一了解是它是某种符合 DispatchType
的类型——因此 仅 重载它可以发送到 func doBar<D : DispatchType>(value: D)
.
如果泛型函数总是由编译器专门化,情况就会不同,因为 test(value:)
的专门实现会知道 value
的具体类型,从而能够选择适当的重载.然而,泛型函数的特化目前只是一种优化(因为没有内联,它会给你的代码增加显着的膨胀),所以这不会改变观察到的行为。
为了允许多态性,一个解决方案是通过添加 doBar()
作为协议要求来利用协议见证 table(参见 this great WWDC talk),并实现专门的它在符合协议的相应 类 中的实现,一般实现是协议扩展的一部分。
这将允许 doBar()
的动态调度,从而允许从 test(value:)
调用它并调用正确的实现。
protocol DispatchType {
func doBar()
}
extension DispatchType {
func doBar() {
print("general function called")
}
}
class DispatchType1: DispatchType {
func doBar() {
print("DispatchType1 called")
}
}
class DispatchType2: DispatchType {
func doBar() {
print("DispatchType2 called")
}
}
func test<D : DispatchType>(value: D) {
value.doBar()
}
let d1 = DispatchType1()
let d2 = DispatchType2()
test(value: d1) // "DispatchType1 called"
test(value: d2) // "DispatchType2 called"