Abe*_*der 5 generics overriding specialization dispatch swift
我的代码遵循以下一般设计:
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")
}
Run Code Online (Sandbox Code Playgroud)
其中,在现实中DispatchType其实是一个后端存储.这些doBar函数是依赖于正确存储类型的优化方法.如果我这样做,一切正常:
let d1 = DispatchType1()
let d2 = DispatchType2()
doBar(value: d1) // "DispatchType1 called"
doBar(value: d2) // "DispatchType2 called"
Run Code Online (Sandbox Code Playgroud)
但是,如果我创建一个调用的函数doBar:
func test<D:DispatchType>(value:D) {
doBar(value: value)
}
Run Code Online (Sandbox Code Playgroud)
我尝试了类似的呼叫模式,我得到:
test(value: d1) // "general function called"
test(value: d2) // "general function called"
Run Code Online (Sandbox Code Playgroud)
这似乎是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")
}
Run Code Online (Sandbox Code Playgroud)
但得到相同的结果.
任何想法,如果这是正确的Swift行为,如果是这样,一个很好的方法来绕过这种行为?
编辑1:我试图避免使用协议的原因示例.假设我有代码(从我的实际代码中大大简化):
protocol Storage {
// ...
}
class Tensor<S:Storage> {
// ...
}
Run Code Online (Sandbox Code Playgroud)
对于Tensor类,我有一组可以在Tensors 上执行的基本操作.但是,操作本身将根据存储更改其行为.目前我完成了这个:
func dot<S:Storage>(_ lhs:Tensor<S>, _ rhs:Tensor<S>) -> Tensor<S> { ... }
Run Code Online (Sandbox Code Playgroud)
虽然我可以把它们放在Tensor课堂上并使用扩展:
extension Tensor where S:CBlasStorage {
func dot(_ tensor:Tensor<S>) -> Tensor<S> {
// ...
}
}
Run Code Online (Sandbox Code Playgroud)
这有一些我不喜欢的副作用:
我觉得dot(lhs, rhs)比较好lhs.dot(rhs).可以编写便利函数来解决这个问题,但这会产生巨大的代码爆炸.
这将导致Tensor该类成为单一的.我真的更喜欢它包含所需的最少量代码并通过辅助功能扩展其功能.
与(2)相关,这意味着任何想要添加新功能的人都必须触及基类,我认为这是糟糕的设计.
编辑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)
}
Run Code Online (Sandbox Code Playgroud)
将导致正确doBar的被调用.这也不是理想的,因为它会导致编写大量额外的代码,但至少让我保留当前的设计.
编辑3:我遇到了文档,显示了使用static泛型的关键字.这至少有点(1):
class Tensor<S:Storage> {
// ...
static func cos(_ tensor:Tensor<S>) -> Tensor<S> {
// ...
}
}
Run Code Online (Sandbox Code Playgroud)
允许你写:
let result = Tensor.cos(value)
Run Code Online (Sandbox Code Playgroud)
它支持运算符重载:
let result = value1 + value2
Run Code Online (Sandbox Code Playgroud)
它确实具有所需的额外冗长度Tensor.这可以通过以下方式改善:
typealias T<S:Storage> = Tensor<S>
Run Code Online (Sandbox Code Playgroud)
这确实是正确的行为,因为重载解析在编译时发生(在运行时发生这将是非常昂贵的操作).因此,从内部来看test(value:),编译器唯一知道的value是它符合某种类型DispatchType- 因此它可以分派的唯一超载是func doBar<D : DispatchType>(value: D).
如果泛型函数总是由编译器专门化,那么情况会有所不同,因为那时专门的实现test(value:)会知道具体的类型,value从而能够选择适当的重载.但是,泛型函数的专业化目前只是一种优化(因为没有内联,它可能会给代码增加大量膨胀),因此这不会改变观察到的行为.
允许多态性的一种解决方案是通过添加协议要求,并在符合协议的各个类中实现它的专用实现来利用协议见证表(参见这个伟大的WWDC对它们的讨论),doBar()一般实现是协议扩展的一部分.
这将允许动态调度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"
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
675 次 |
| 最近记录: |