Uso de protocolos como tipos de matriz y parámetros de función en swift

137

Quiero crear una clase que pueda almacenar objetos que se ajusten a un determinado protocolo. Los objetos deben almacenarse en una matriz escrita. De acuerdo con la documentación de Swift, los protocolos se pueden usar como tipos: 

Debido a que es un tipo, puede usar un protocolo en muchos lugares donde se permiten otros tipos, incluidos:

  • Como tipo de parámetro o tipo de retorno en una función, método o inicializador
  • Como el tipo de una constante, variable o propiedad
  • Como el tipo de elementos en una matriz, diccionario u otro contenedor

Sin embargo, lo siguiente genera errores de compilación:

El protocolo 'SomeProtocol' solo puede usarse como una restricción genérica porque tiene requisitos de tipo Self o asociados

¿Cómo se supone que debes resolver esto?

protocol SomeProtocol: Equatable {
    func bla()
}

class SomeClass {
    
    var protocols = [SomeProtocol]()
    
    func addElement(element: SomeProtocol) {
        self.protocols.append(element)
    }
    
    func removeElement(element: SomeProtocol) {
        if let index = find(self.protocols, element) {
            self.protocols.removeAtIndex(index)
        }
    }
}
snod
fuente
2
En Swift hay una clase especial de protocolos que no proporciona polimorfismo sobre los tipos que lo implementan. Dichos protocolos utilizan Self o tipo asociado en su definición (y Equatable es uno de ellos). En algunos casos, es posible usar un contenedor de tipo borrado para hacer que su colección sea homomórfica. Mira aquí por ejemplo.
werediver

Respuestas:

48

Has encontrado una variante de un problema con los protocolos en Swift para los cuales todavía no existe una buena solución.

Consulte también Matriz extensible para verificar si está ordenado en Swift? , contiene sugerencias sobre cómo solucionarlo que pueden ser adecuadas para su problema específico (su pregunta es muy genérica, tal vez pueda encontrar una solución usando estas respuestas).

Polvo oscuro
fuente
1
Creo que esta es la respuesta correcta por el momento. La solución de Nate está funcionando pero no resuelve mi problema por completo.
snod
32

Desea crear una clase genérica, con una restricción de tipo que requiera que las clases utilizadas con ella se ajusten SomeProtocol, de esta manera:

class SomeClass<T: SomeProtocol> {
    typealias ElementType = T
    var protocols = [ElementType]()

    func addElement(element: ElementType) {
        self.protocols.append(element)
    }

    func removeElement(element: ElementType) {
        if let index = find(self.protocols, element) {
            self.protocols.removeAtIndex(index)
        }
    }
}
Nate Cook
fuente
¿Cómo crearías una instancia de un objeto de esa clase?
snod
Hmmm ... de esta manera te encierra en un solo tipo que se ajusta a SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Nate Cook
De esta manera, ¿solo podría agregar objetos de clase MyMemberClassa la matriz?
snod
olet foo = SomeClass<MyMemberClass>()
DarkDust
@snod Sí, que no es lo que estás buscando. El problema es la Equatableconformidad: sin eso puede usar su código exacto. Tal vez presentar una solicitud de error / función?
Nate Cook
15

En Swift hay una clase especial de protocolos que no proporciona polimorfismo sobre los tipos que lo implementan. Dichos protocolos usan Selfo associatedtypepalabras clave en sus definiciones (y Equatablees una de ellas).

En algunos casos, es posible usar un contenedor de tipo borrado para hacer que su colección sea homomórfica. A continuación se muestra un ejemplo.

// This protocol doesn't provide polymorphism over the types which implement it.
protocol X: Equatable {
    var x: Int { get }
}

// We can't use such protocols as types, only as generic-constraints.
func ==<T: X>(a: T, b: T) -> Bool {
    return a.x == b.x
}

// A type-erased wrapper can help overcome this limitation in some cases.
struct AnyX {
    private let _x: () -> Int
    var x: Int { return _x() }

    init<T: X>(_ some: T) {
        _x = { some.x }
    }
}

// Usage Example

struct XY: X {
    var x: Int
    var y: Int
}

struct XZ: X {
    var x: Int
    var z: Int
}

let xy = XY(x: 1, y: 2)
let xz = XZ(x: 3, z: 4)

//let xs = [xy, xz] // error
let xs = [AnyX(xy), AnyX(xz)]
xs.forEach { print($0.x) } // 1 3
werediver
fuente
12

La solución limitada que encontré es marcar el protocolo como un protocolo de clase solamente. Esto le permitirá comparar objetos usando el operador '==='. Entiendo que esto no funcionará para estructuras, etc., pero fue lo suficientemente bueno en mi caso.

protocol SomeProtocol: class {
    func bla()
}

class SomeClass {

    var protocols = [SomeProtocol]()

    func addElement(element: SomeProtocol) {
        self.protocols.append(element)
    }

    func removeElement(element: SomeProtocol) {
        for i in 0...protocols.count {
            if protocols[i] === element {
                protocols.removeAtIndex(i)
                return
            }
        }
    }

}
almas
fuente
¿No permite esto entradas duplicadas protocolssi addElementse llama más de una vez con el mismo objeto?
Tom Harrington
Sí, las matrices en swift pueden contener entradas duplicadas. Si cree que esto puede suceder en su código, use el conjunto en lugar de la matriz o asegúrese de que la matriz no contenga ese objeto.
almas
Puede llamar removeElement()antes de agregar el nuevo elemento si desea evitar duplicados.
Georgios
Me refiero a cómo controlas que tu matriz está en el aire, ¿verdad? Gracias por la respuesta
Reimond Hill
9

La solución es bastante simple:

protocol SomeProtocol {
    func bla()
}

class SomeClass {
    init() {}

    var protocols = [SomeProtocol]()

    func addElement<T: SomeProtocol where T: Equatable>(element: T) {
        protocols.append(element)
    }

    func removeElement<T: SomeProtocol where T: Equatable>(element: T) {
        protocols = protocols.filter {
            if let e = $0 as? T where e == element {
                return false
            }
            return true
        }
    }
}
bzz
fuente
44
Te perdiste lo importante: el OP quiere que el protocolo herede el Equatableprotocolo. Hace una gran diferencia.
werediver
@werediver No lo creo. Quiere almacenar objetos conformes SomeProtocolen una matriz tipada. Equatablela conformidad se requiere solo para eliminar elementos de la matriz. Mi solución es una versión mejorada de la solución @almas porque se puede usar con cualquier tipo Swift que se ajuste al Equatableprotocolo.
bzz
2

Supongo que su objetivo principal es mantener una colección de objetos que se ajusten a algún protocolo, agregar a esta colección y eliminarla. Esta es la funcionalidad como se indica en su cliente, "SomeClass". La herencia equitativa requiere autocontrol y eso no es necesario para esta funcionalidad. Podríamos haber hecho que esto funcione en matrices en Obj-C usando la función "índice" que puede tomar un comparador personalizado, pero esto no es compatible con Swift. Entonces, la solución más simple es usar un diccionario en lugar de una matriz como se muestra en el código a continuación. He proporcionado getElements () que le devolverá la matriz de protocolos que desea. Entonces, cualquiera que use SomeClass ni siquiera sabría que se utilizó un diccionario para la implementación.

Como en cualquier caso, necesitaría alguna propiedad distintiva para separar sus objetos, he asumido que es "nombre". Asegúrese de que su elemento do.name = "foo" cuando cree una nueva instancia de SomeProtocol. Si el nombre no está establecido, aún puede crear la instancia, pero no se agregará a la colección y addElement () devolverá "falso".

protocol SomeProtocol {
    var name:String? {get set} // Since elements need to distinguished, 
    //we will assume it is by name in this example.
    func bla()
}

class SomeClass {

    //var protocols = [SomeProtocol]() //find is not supported in 2.0, indexOf if
     // There is an Obj-C function index, that find element using custom comparator such as the one below, not available in Swift
    /*
    static func compareProtocols(one:SomeProtocol, toTheOther:SomeProtocol)->Bool {
        if (one.name == nil) {return false}
        if(toTheOther.name == nil) {return false}
        if(one.name ==  toTheOther.name!) {return true}
        return false
    }
   */

    //The best choice here is to use dictionary
    var protocols = [String:SomeProtocol]()


    func addElement(element: SomeProtocol) -> Bool {
        //self.protocols.append(element)
        if let index = element.name {
            protocols[index] = element
            return true
        }
        return false
    }

    func removeElement(element: SomeProtocol) {
        //if let index = find(self.protocols, element) { // find not suported in Swift 2.0


        if let index = element.name {
            protocols.removeValueForKey(index)
        }
    }

    func getElements() -> [SomeProtocol] {
        return Array(protocols.values)
    }
}
Jitendra Kulkarni
fuente
0

Encontré una solución Swift no pura y pura en esa publicación de blog: http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/

El truco es ajustarse a NSObjectProtocollo que presenta isEqual(). Por lo tanto, en lugar de usar el Equatableprotocolo y su uso predeterminado de== podría escribir su propia función para encontrar el elemento y eliminarlo.

Aquí está la implementación de su find(array, element) -> Int?función:

protocol SomeProtocol: NSObjectProtocol {

}

func find(protocols: [SomeProtocol], element: SomeProtocol) -> Int? {
    for (index, object) in protocols.enumerated() {
        if (object.isEqual(element)) {
            return index
        }
    }

    return nil
}

Nota: En este caso, sus objetos conformes a SomeProtocoldeben heredar de NSObject.

Kevin Delord
fuente