我有遵循以下常规设计的代码:
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> {
// ...
}
}
这有一些我不喜欢的副作用:
我认为
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)
}
将导致正确的
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()
作为协议要求,并在符合该协议的各个类中实现它的专门实现,从而利用协议见证表(请参见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"