arrays - type - generic swift
Matrices de genéricos en Swift (5)
He estado jugando con matrices de clases genéricas con diferentes tipos. Es más fácil explicar mi problema con un código de muestra:
// Obviously a very pointless protocol...
protocol MyProtocol {
var value: Self { get }
}
extension Int : MyProtocol { var value: Int { return self } }
extension Double: MyProtocol { var value: Double { return self } }
class Container<T: MyProtocol> {
var values: [T]
init(_ values: T...) {
self.values = values
}
func myMethod() -> [T] {
return values
}
}
Ahora si trato de crear una matriz de contenedores así:
var containers: [Container<MyProtocol>] = []
Me sale el error:
El protocolo ''MyProtocol'' solo se puede usar como una restricción genérica porque tiene requisitos de tipo Self o asociados.
Para solucionar esto, puedo usar
[AnyObject]
:
let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0)]
// Explicitly stating the types just for clarity.
Pero ahora surge otro ''problema'' al enumerar a través de
containers
:
for container in containers {
if let c = container as? Container<Int> {
println(c.myMethod())
} else if let c = container as? Container<Double> {
println(c.myMethod())
}
}
Como puede ver en el código anterior, después de determinar el tipo de
container
, se llama al mismo método en ambos casos.
Mi pregunta es:
¿Hay una mejor manera de obtener el
Container
con el tipo correcto que lanzar a cada tipo posible de
Container
?
¿O hay algo más que he pasado por alto?
Cambié la declaración de matriz para que sea una matriz de AnyObject para que se pueda usar filter, map y reduce (y también agregué algunos objetos más para verificar).
let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0), "Hello", "World", 42]
Esto le permitirá verificar el tipo de matriz y el filtro antes de recorrer la matriz.
let strings = containers.filter({ return ($0 is String) })
println(strings) // [Hello, World]
for ints in containers.filter({ return ($0 is Int) }) {
println("Int is /(foo)") // Int is 42
}
let ints = containers.filter({ return ($0 is Container<Int>) })
// as this array is known to only contain Container<Int> instances, downcast and unwrap directly
for i in ints as [Container<Int>] {
// do stuff
println(i.values) // [1, 2, 3]
}
Este es un buen ejemplo de "¿qué querías que pasara?" Y en realidad demuestra la complejidad que explota si Swift tenía tipos realmente de primera clase.
protocol MyProtocol {
var value: Self { get }
}
Excelente.
MyProtocol.value
devuelve cualquier tipo que lo implemente, recordando que esto debe determinarse en tiempo de compilación, no en tiempo de ejecución.
var containers: [Container<MyProtocol>] = []
Entonces, determinado en tiempo de compilación, ¿qué tipo es este? Olvida el compilador, solo hazlo en papel. Sí, no estoy seguro de qué tipo sería. Me refiero al tipo concreto . Sin metatipos.
let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0)]
Usted sabe que va por el camino equivocado cuando
AnyObject
se coló en sus firmas.
Nada de esto va a funcionar.
Después de
AnyObject
es solo cilicio.
¿O hay algo más que he pasado por alto?
Si. Necesita un tipo y no ha proporcionado uno. Ha proporcionado una regla para restringir un tipo, pero no un tipo real. Regrese a su problema real y piense más profundamente. (El análisis de metatipos casi nunca es su problema "real" a menos que esté trabajando en un doctorado CS, en cuyo caso lo estaría haciendo en Idris, no en Swift). ¿Qué problema real está resolviendo?
Esto se puede explicar mejor con protocolos como
Equatable
.
No puede declarar una matriz
[Equatable]
porque si bien dos instancias
Int
pueden compararse entre sí y dos instancias de
Double
pueden compararse entre sí, no puede comparar un
Int
con un
Double
aunque ambos implementen
Equatable
.
MyProtocol
es un protocolo, lo que significa que proporciona una interfaz genérica.
Desafortunadamente, también has usado
Self
en la definición.
Eso significa que cada tipo que se ajuste a
MyProtocol
lo implementará de manera diferente.
Lo ha escrito usted mismo:
Int
tendrá un
value
como
value
var value: Int
mientras que un
MyObject
tendrá un
value
como
var value: MyObject
.
Eso significa que una estructura / clase que se ajusta a
MyProtocol
no se puede usar en lugar de otra estructura / clase que se ajuste a
MyProtocol
.
Eso también significa que no puede usar
MyProtocol
de esta manera, sin especificar un tipo concreto.
Si reemplaza ese
Self
con un tipo concreto, por ejemplo,
AnyObject
, funcionará.
Sin embargo, actualmente (Xcode 6.3.1) desencadena un error de segmentación al compilar).
Hay una forma, más o menos, de hacer lo que quieres, más o menos. Hay una manera, con protocolos, de eliminar la restricción de tipo y aún así obtener el resultado que desea, más o menos, pero no siempre es bonito. Esto es lo que se me ocurrió como protocolo en su situación:
protocol MyProtocol {
func getValue() -> Self
}
extension Int: MyProtocol {
func getValue() -> Int {
return self
}
}
extension Double: MyProtocol {
func getValue() -> Double {
return self
}
}
Tenga en cuenta que la propiedad de
value
que colocó originalmente en su declaración de protocolo se ha cambiado a un método que devuelve el objeto.
Eso no es muy interesante.
Pero ahora, debido a que se ha deshecho de la propiedad
value
en el protocolo,
MyProtocol
puede usarse como un tipo, no solo como una restricción de tipo.
Su clase de
Container
ya no necesita ser genérica.
Puedes declararlo así:
class Container {
var values: [MyProtocol]
init(_ values: MyProtocol...) {
self.values = values
}
func myMethod() -> [MyProtocol] {
return values
}
}
Y debido a que
Container
ya no es genérico, puede crear una
Array
de
Container
e iterar a través de ellos, imprimiendo los resultados del método
myMethod()
:
var containers = [Container]()
containers.append(Container(1, 4, 6, 2, 6))
containers.append(Container(1.2, 3.5))
for container in containers {
println(container.myMethod())
}
// Output: [1, 4, 6, 2, 6]
// [1.2, 3.5]
El truco consiste en construir un protocolo que solo incluya funciones genéricas y no establezca otros requisitos en un tipo conforme. Si puede salirse con la suya, puede usar el protocolo como un tipo, y no solo como una restricción de tipo.
Y como
MyProtocol
adicional (si quiere llamarlo así), su matriz de valores
MyProtocol
puede incluso mezclar diferentes tipos que se ajustan a
MyProtocol
.
Entonces, si le da a
String
una extensión
MyProtocol
como esta:
extension String: MyProtocol {
func getValue() -> String {
return self
}
}
En realidad, puede inicializar un
Container
con tipos mixtos:
let container = Container(1, 4.2, "no kidding, this works")
[Advertencia: estoy probando esto en uno de los parques infantiles en línea. Todavía no he podido probarlo en Xcode ...]
Editar:
Si aún desea que el
Container
sea genérico y solo contenga un tipo de objeto, puede lograrlo haciendo que
se
ajuste a su propio protocolo:
protocol ContainerProtocol {
func myMethod() -> [MyProtocol]
}
class Container<T: MyProtocol>: ContainerProtocol {
var values: [T] = []
init(_ values: T...) {
self.values = values
}
func myMethod() -> [MyProtocol] {
return values.map { $0 as MyProtocol }
}
}
Ahora
todavía
puede tener una matriz de objetos
[ContainerProtocol]
e iterar a través de ellos invocando
myMethod()
:
let containers: [ContainerProtocol] = [Container(5, 3, 7), Container(1.2, 4,5)]
for container in containers {
println(container.myMethod())
}
Tal vez eso todavía no funcione para usted, pero ahora el
Container
está restringido a un solo tipo, y aún así puede iterar a través de una matriz de objetos
ContainterProtocol
.
Si prueba este ejemplo modificado en un parque infantil, se bloqueará sistemáticamente:
// Obviously a very pointless protocol...
protocol MyProtocol {
var value: Int { get }
}
extension Int : MyProtocol { var value: Int { return self } }
//extension Double: MyProtocol { var value: Double { return self } }
class Container<T: MyProtocol> {
var values: [T]
init(_ values: T...) {
self.values = values
}
}
var containers: [Container<MyProtocol>] = []
Probablemente todavía estén trabajando en esto, y las cosas podrían cambiar en el futuro.
De todos modos, a partir de ahora, mi explicación para esto es que un protocolo no es un
tipo concreto
.
Por lo tanto, ahora no sabe cuánto espacio ocupará en RAM algo que se ajuste al protocolo (por ejemplo, un
Int
podría no ocupar la misma cantidad de RAM que un
Double
).
Por lo tanto, podría ser un problema bastante complicado la asignación de la matriz en ram.
Al usar un
NSArray
, se le
NSArray
una matriz de punteros (punteros a
NSObjects
) y todos ellos ocupan la misma cantidad de ram.
Puede pensar en
NSArray
como una matriz del
tipo concreto
"puntero a
NSObject
".
Por lo tanto, no hay problema para calcular la asignación de RAM.
Considere que
Array
y
Dictionary
en Swift son
estructuras genéricas
, no
objetos que contienen punteros a objetos
como en Obj-C.
Espero que esto ayude.