Использование протоколов в качестве типов массивов и параметров функций в swift

137

Я хочу создать класс, который может хранить объекты, соответствующие определенному протоколу. Объекты должны храниться в типизированном массиве. Согласно документации Swift протоколы могут использоваться как типы: 

Поскольку это тип, вы можете использовать протокол во многих местах, где разрешены другие типы, включая:

  • Как тип параметра или тип возвращаемого значения в функции, методе или инициализаторе
  • Как тип константы, переменной или свойства
  • Как тип элементов в массиве, словаре или другом контейнере

Однако следующее генерирует ошибки компилятора:

Протокол 'SomeProtocol' может использоваться только в качестве общего ограничения, поскольку он имеет Self или требования к связанному типу.

Как вы должны решить это:

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
источник
2
В Swift есть специальный класс протоколов, который не обеспечивает полиморфизм над типами, которые его реализуют. Такие протоколы используют Self или связанный тип в своем определении (и Equatable является одним из них). В некоторых случаях можно использовать обертку со стертым типом, чтобы сделать вашу коллекцию гомоморфной. Посмотрите здесь, например.
werediver

Ответы:

48

Вы столкнулись с вариантом проблемы с протоколами в Swift, для которой пока не найдено хорошего решения.

См. Также Расширение массива, чтобы проверить, отсортирован ли он в Swift? он содержит предложения о том, как обойти эту проблему, которая может подойти для вашей конкретной проблемы (ваш вопрос очень общий, возможно, вы можете найти обходной путь, используя эти ответы).

DarkDust
источник
1
Я думаю, что это правильный ответ на данный момент. Решение Нейта работает, но не решает мою проблему полностью.
snod
32

Вы хотите создать универсальный класс с ограничением типа, которое требует, чтобы используемые классы соответствовали ему SomeProtocol, например так:

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)
        }
    }
}
Нейт Кук
источник
Как бы вы создали экземпляр объекта этого класса?
snod
Хммм ... Таким образом, вы блокируете использование одного типа, который соответствует SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Нейт Кук
Таким образом, вы можете добавить только объекты класса MyMemberClassв массив?
snod
илиlet foo = SomeClass<MyMemberClass>()
DarkDust
@snod Да, это не то, что вы ищете. Проблема заключается в Equatableсоответствии - без этого вы можете использовать свой точный код. Может быть, отправить запрос об ошибке / функции?
Нейт Кук
15

В Swift есть специальный класс протоколов, который не обеспечивает полиморфизм над типами, которые его реализуют. Такие протоколы используют Selfили associatedtypeключевые слова в своих определениях (и Equatableявляется одним из них).

В некоторых случаях можно использовать обертку со стертым типом, чтобы сделать вашу коллекцию гомоморфной. Ниже приведен пример.

// 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
источник
12

Ограниченное решение, которое я нашел, - пометить протокол как протокол только для класса. Это позволит вам сравнивать объекты с помощью оператора «===». Я понимаю, что это не сработает для структур и т. Д., Но в моем случае это было достаточно хорошо.

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
            }
        }
    }

}
алмас
источник
Разве это не позволяет дублировать записи protocols, если addElementвызывается более одного раза с одним и тем же объектом?
Том Харрингтон
Да, массивы в swift могут содержать повторяющиеся записи. Если вы думаете, что это может произойти в вашем коде, либо используйте Set вместо массива, либо убедитесь, что массив уже не содержит этот объект.
Almas
Вы можете позвонить removeElement()до добавления нового элемента, если хотите избежать дублирования.
Георгиос
Я имею в виду, как вы контролируете, что ваш массив находится в воздухе, верно? Спасибо за ответ
Реймонд Хилл
9

Решение довольно простое:

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
источник
4
Вы упустили важную вещь: ОП хочет, чтобы протокол наследовал Equatableпротокол. Это имеет огромное значение.
werediver
@ ответа я так не думаю. Он хочет хранить объекты в соответствии с SomeProtocolтипизированным массивом. Equatableсоответствие требуется только для удаления элементов из массива. Мое решение - улучшенная версия решения @almas, потому что его можно использовать с любым типом Swift, соответствующим Equatableпротоколу.
BZZ
2

Я полагаю, что ваша основная цель - хранить коллекцию объектов, соответствующую какому-либо протоколу, добавлять в эту коллекцию и удалять из нее. Это функциональность, как указано в вашем клиенте "SomeClass". Равное наследование требует себя, и это не нужно для этой функциональности. Мы могли бы выполнить эту работу в массивах в Obj-C, используя функцию «index», которая может принимать пользовательский компаратор, но это не поддерживается в Swift. Поэтому самое простое решение - использовать словарь вместо массива, как показано в коде ниже. Я предоставил getElements (), который вернет вам требуемый массив протоколов. Поэтому любой, кто использует SomeClass, даже не знает, что для реализации использовался словарь.

Поскольку в любом случае вам понадобится какое-то отличительное свойство для разделения ваших объектов, я предположил, что это «имя». Пожалуйста, убедитесь, что ваш do element.name = "foo" при создании нового экземпляра SomeProtocol. Если имя не задано, вы все равно можете создать экземпляр, но он не будет добавлен в коллекцию, а addElement () вернет «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)
    }
}
Джитендра Кулкарни
источник
0

Я нашел не совсем чистое решение Swift в этом посте: http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/

Хитрость заключается в том, чтобы соответствовать, NSObjectProtocolкак это вводит isEqual(). Поэтому вместо использования Equatableпротокола и его использования по умолчанию== вы можете написать свою собственную функцию, чтобы найти элемент и удалить его.

Вот реализация вашей find(array, element) -> Int?функции:

protocol SomeProtocol: NSObjectProtocol {

}

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

    return nil
}

Примечание. В этом случае ваши объекты, соответствующие требованиям, SomeProtocolдолжны наследоваться от NSObject.

Кевин Делорд
источник