used type only has generic constraint because associated arrays swift generics swift-protocols

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.