Uso de protocolos como tipos de matriz e parâmetros de função em rápida

137

Eu quero criar uma classe que possa armazenar objetos em conformidade com um determinado protocolo. Os objetos devem ser armazenados em uma matriz digitada. De acordo com a documentação da Swift, os protocolos podem ser usados ​​como tipos: 

Por ser um tipo, você pode usar um protocolo em muitos lugares onde outros tipos são permitidos, incluindo:

  • Como um tipo de parâmetro ou tipo de retorno em uma função, método ou inicializador
  • Como o tipo de uma constante, variável ou propriedade
  • Como o tipo de itens em uma matriz, dicionário ou outro contêiner

No entanto, o seguinte gera erros do compilador:

O protocolo 'SomeProtocol' só pode ser usado como uma restrição genérica porque possui requisitos de tipo Próprio ou associado

Como você deve resolver isso:

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)
        }
    }
}
rude
fonte
2
Em Swift, existe uma classe especial de protocolos que não fornece polimorfismo sobre os tipos que o implementam. Tais protocolos usam Self ou tipo associado em sua definição (e Equatable é um deles). Em alguns casos, é possível usar um invólucro apagado por tipo para tornar sua coleção homomórfica. Veja aqui por exemplo.
werediver

Respostas:

48

Você encontrou uma variante de um problema com protocolos no Swift para os quais ainda não existe uma boa solução.

Consulte também Estendendo a matriz para verificar se está classificada no Swift? , ele contém sugestões sobre como solucionar o problema que pode ser adequado ao seu problema específico (sua pergunta é muito genérica, talvez você possa encontrar uma solução alternativa usando essas respostas).

DarkDust
fonte
1
Eu acho que esta é a resposta correta para o momento. A solução de Nate está funcionando, mas não resolve meu problema completamente.
snod
32

Você deseja criar uma classe genérica, com uma restrição de tipo que requer que as classes usadas com ela estejam em conformidade SomeProtocol, assim:

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
fonte
Como você instanciaria um objeto dessa classe?
snod
Hmmm ... esta maneira fechaduras-lo a usar um único tipo em conformidade com SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Nate Cozinhe
Dessa forma, você só pode adicionar objetos de classe MyMemberClassà matriz?
snod
oulet foo = SomeClass<MyMemberClass>()
DarkDust
@ Snod Sim, que não é o que você está procurando. O problema é Equatableconformidade - sem isso você pode usar seu código exato. Talvez arquive uma solicitação de bug / recurso?
Nate Cook
15

Em Swift, existe uma classe especial de protocolos que não fornece polimorfismo sobre os tipos que o implementam. Tais protocolos usam Selfou associatedtypepalavras - chave em suas definições (eEquatable é um deles).

Em alguns casos, é possível usar um invólucro de tipo apagado para tornar sua coleção homomórfica. Abaixo está um exemplo.

// 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
mergulhador
fonte
12

A solução limitada que encontrei é marcar o protocolo como um protocolo somente de classe. Isso permitirá comparar objetos usando o operador '==='. Entendo que isso não funcionará para estruturas, etc., mas foi bom o suficiente no meu 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
fonte
Isso não permite entradas duplicadas protocols, se addElementfor chamado mais de uma vez com o mesmo objeto?
Tom Harrington
Sim, matrizes em rápido podem conter entradas duplicadas. Se você acha que isso pode acontecer no seu código, use o conjunto em vez de matriz ou verifique se a matriz ainda não contém esse objeto.
Almas
Você pode ligar removeElement()antes de acrescentar o novo elemento se desejar evitar duplicatas.
Georgios
Quero dizer, como você controla sua matriz está no ar, certo? Obrigado pela resposta
Reimond Colina
9

A solução é bem simples:

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
fonte
4
Você perdeu o importante: o OP quer que o protocolo herde o Equatableprotocolo. Faz uma enorme diferença.
werediver
@werediver Acho que não. Ele deseja armazenar objetos conformes SomeProtocolem uma matriz digitada. Equatablea conformidade é necessária apenas para remover elementos da matriz. Minha solução é uma versão aprimorada da solução @almas, pois pode ser usada com qualquer tipo Swift que esteja em conformidade com o Equatableprotocolo.
bzz
2

Suponho que seu objetivo principal seja armazenar uma coleção de objetos em conformidade com algum protocolo, adicionar a essa coleção e excluir dela. Essa é a funcionalidade conforme declarada no seu cliente, "SomeClass". A herança equivalente requer auto e isso não é necessário para esta funcionalidade. Poderíamos ter feito esse trabalho em matrizes no Obj-C usando a função "index" que pode levar um comparador personalizado, mas isso não é suportado no Swift. Portanto, a solução mais simples é usar um dicionário em vez de uma matriz, conforme mostrado no código abaixo. Eu forneci getElements (), que retornará a matriz de protocolo que você queria. Portanto, qualquer pessoa que use o SomeClass nem saberia que um dicionário foi usado para implementação.

Como em qualquer caso, você precisaria de alguma propriedade distinta para separar seus objetivos, presumi que seja "nome". Certifique-se de que você faça element.name = "foo" ao criar uma nova instância de SomeProtocol. Se o nome não estiver definido, você ainda poderá criar a instância, mas ela não será adicionada à coleção e addElement () retornará "false".

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
fonte
0

Encontrei uma solução Swift não pura pura nessa postagem do blog: http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/

O truque é se conformar com o NSObjectProtocolque é introduzido isEqual(). Portanto, em vez de usar o Equatableprotocolo e seu uso padrão de== você pode escrever sua própria função para encontrar o elemento e removê-lo.

Aqui está a implementação da sua find(array, element) -> Int?função:

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: Nesse caso, seus objetos conformes SomeProtocoldevem herdar deNSObject .

Kevin Delord
fonte