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


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.