从间接调用在Swift 3中调用错误的专用泛型函数

时间:2017-02-01 12:35:53

标签: swift generics override dispatch specialization

我的代码遵循以下的一般设计:

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类,我有一组可以在Tensor上执行的基本操作。但是,操作本身将根据存储更改其行为。目前我通过以下方式实现这一目标:

func dot<S:Storage>(_ lhs:Tensor<S>, _ rhs:Tensor<S>) -> Tensor<S> { ... }

虽然我可以将它们放在Tensor类中并使用扩展名:

extension Tensor where S:CBlasStorage {
    func dot(_ tensor:Tensor<S>) -> Tensor<S> {
       // ...
    }
}

这有一些我不喜欢的副作用:

  1. 我认为dot(lhs, rhs)优于lhs.dot(rhs)。可以编写便利函数来解决这个问题,但这会产生巨大的代码爆炸。

  2. 这将导致Tensor类变成单一的。我真的更喜欢让它包含所需的最少代码,并通过辅助功能扩展其功能。

  3. 与(2)相关,这意味着任何想要添加新功能的人都必须触及我认为设计不好的基类。

  4. 编辑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>
    

1 个答案:

答案 0 :(得分:7)

这确实是正确的行为,因为重载解析在编译时发生(在运行时发生这将是非常昂贵的操作)。因此,从test(value:)开始,编译器对value的唯一了解就是它符合DispatchType的某种类型 - 因此只有 它可以调度到的重载是func doBar<D : DispatchType>(value: D)

如果泛型函数始终由编译器专门化,那么情况会有所不同,因为test(value:)的专门实现会知道value的具体类型,因此能够选择适当的重载。但是,泛型函数的专业化目前只是一种优化(因为没有内联,它可能会给代码增加很多膨胀),所以这不会改变观察到的行为。

为了允许多态,一种解决方案是通过添加doBar()作为协议要求来利用协议见证表(请参阅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"