Використання протоколів як типів масивів та функціональних параметрів швидко


136

Я хочу створити клас, який може зберігати об'єкти, що відповідають певному протоколу. Об'єкти повинні зберігатися в набраному масиві. Згідно з документацією 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)
        }
    }
}

2
У Swift існує спеціальний клас протоколів, який не забезпечує поліморфізму щодо типів, які його реалізують. Такі протоколи використовують Self або асоційований тип у своєму визначенні (і Equatable є одним з них). У деяких випадках можна використовувати оброблювану машину зі стертою шрифтом, щоб зробити колекцію гомоморфною. Подивіться тут, наприклад.
werediver

Відповіді:


48

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

Дивіться також Розширення масиву, щоб перевірити, чи він відсортований у Swift? , він містить пропозиції про те, як вирішити цю проблему, що може відповідати вашій конкретній проблемі (ваше запитання дуже загальне; можливо, ви можете знайти вирішення, використовуючи ці відповіді).


1
Я думаю, що це правильна відповідь на даний момент. Рішення Nate працює, але не вирішує мою проблему повністю.
снод

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

Як би ви створили об'єкт цього класу?
снор

Гммм ... Цей спосіб замикає вас на використанні одного типу, який відповідає SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Нейт Кук

Таким чином ви могли лише додати об’єкти класу MyMemberClassдо масиву?
сноп

або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

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()перш ніж додавати новий елемент, якщо хочете уникнути дублікатів.
Георгіос

Я маю на увазі, як ви керуєте тим, що ваш масив знаходиться в повітрі, правда? Дякую за відповідь
Reimond Hill

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

4
Ви пропустили важливе: ОП хоче, щоб протокол успадкував Equatableпротокол. Це робить величезну різницю.
werediver

@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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.