La función genérica especializada incorrecta se llama en Swift 3 desde una llamada indirecta
generics overriding (1)
Tengo un código que sigue el diseño general de:
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")
}
donde en realidad
DispatchType
es en realidad un almacenamiento de fondo.
Las funciones
doBar
son métodos optimizados que dependen del tipo de almacenamiento correcto.
Todo funciona bien si lo hago:
let d1 = DispatchType1()
let d2 = DispatchType2()
doBar(value: d1) // "DispatchType1 called"
doBar(value: d2) // "DispatchType2 called"
Sin embargo, si hago una función que llama a
doBar
:
func test<D:DispatchType>(value:D) {
doBar(value: value)
}
e intento un patrón de llamada similar, obtengo:
test(value: d1) // "general function called"
test(value: d2) // "general function called"
Esto parece algo que Swift debería poder manejar, ya que debería poder determinar en tiempo de compilación las restricciones de tipo.
Solo como una prueba rápida, también intenté escribir
doBar
como:
func doBar<D:DispatchType>(value:D) where D:DispatchType1 {
print("DispatchType1 called")
}
func doBar<D:DispatchType>(value:D) where D:DispatchType2 {
print("DispatchType2 called")
}
pero obtén los mismos resultados.
¿Alguna idea de si este es el comportamiento correcto de Swift, y si es así, una buena manera de evitar este comportamiento?
Edición 1 : Ejemplo de por qué estaba tratando de evitar el uso de protocolos. Supongamos que tengo el código (muy simplificado de mi código real):
protocol Storage {
// ...
}
class Tensor<S:Storage> {
// ...
}
Para la clase
Tensor
, tengo un conjunto básico de operaciones que se pueden realizar en el
Tensor
.
Sin embargo, las operaciones mismas cambiarán su comportamiento en función del almacenamiento.
Actualmente lo logro con:
func dot<S:Storage>(_ lhs:Tensor<S>, _ rhs:Tensor<S>) -> Tensor<S> { ... }
Si bien puedo ponerlos en la clase
Tensor
y usar extensiones:
extension Tensor where S:CBlasStorage {
func dot(_ tensor:Tensor<S>) -> Tensor<S> {
// ...
}
}
Esto tiene algunos efectos secundarios que no me gustan:
-
Creo que
dot(lhs, rhs)
es preferible alhs.dot(rhs)
. Se pueden escribir funciones de conveniencia para evitar esto, pero eso creará una gran explosión de código. -
Esto hará que la clase
Tensor
vuelva monolítica. Realmente prefiero que contenga la cantidad mínima de código necesaria y expanda su funcionalidad mediante funciones auxiliares. -
En relación con (2), esto significa que cualquiera que quiera agregar una nueva funcionalidad tendrá que tocar la clase base, lo que considero un mal diseño.
Edición 2 : una alternativa es que las cosas funcionen de manera esperada si usa restricciones para todo:
func test<D:DispatchType>(value:D) where D:DispatchType1 {
doBar(value: value)
}
func test<D:DispatchType>(value:D) where D:DispatchType2 {
doBar(value: value)
}
hará que se
doBar
al
doBar
correcto.
Esto tampoco es ideal, ya que hará que se escriba mucho código adicional, pero al menos me permite mantener mi diseño actual.
Edición 3
: Encontré documentación que muestra el uso de palabras clave
static
con genéricos.
Esto ayuda al menos con el punto (1):
class Tensor<S:Storage> {
// ...
static func cos(_ tensor:Tensor<S>) -> Tensor<S> {
// ...
}
}
te permite escribir:
let result = Tensor.cos(value)
y es compatible con la sobrecarga del operador:
let result = value1 + value2
tiene la verbosidad añadida del
Tensor
requerido.
Esto puede mejorar un poco con:
typealias T<S:Storage> = Tensor<S>
De hecho, este es el comportamiento correcto, ya que la resolución de sobrecarga tiene lugar en tiempo de compilación (sería una operación bastante costosa en tiempo de ejecución).
Por lo tanto, desde dentro de la
test(value:)
, lo único que el compilador sabe sobre el
value
es que es de algún tipo que se ajusta a
DispatchType
; por lo tanto, la
única
sobrecarga a la que puede enviar es
func doBar<D : DispatchType>(value: D)
.
Las cosas serían diferentes si el compilador especializara siempre las funciones genéricas, porque entonces una implementación especializada de
test(value:)
conocería el tipo concreto de
value
y, por lo tanto, podría elegir la sobrecarga adecuada.
Sin embargo, la especialización de las funciones genéricas es actualmente solo una optimización (ya que sin la inclusión en línea, puede agregar una hinchazón significativa a su código), por lo que esto no cambia el comportamiento observado.
Una solución para permitir el polimorfismo es aprovechar la tabla de testigos del protocolo (vea
esta gran charla de WWDC
sobre ellos) agregando
doBar()
como un requisito del protocolo e implementando implementaciones especializadas en las clases respectivas que se ajustan al protocolo , con la implementación general como parte de la extensión del protocolo.
Esto permitirá el despacho dinámico de
doBar()
, permitiendo así que se
doBar()
desde
test(value:)
y se llame a la implementación correcta.
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"