Як в Swift я можу оголосити змінну певного типу, яка відповідає одному або декільком протоколам?


96

У Swift я можу явно встановити тип змінної, оголосивши її наступним чином:

var object: TYPE_NAME

Якщо ми хочемо зробити крок далі і оголосити змінну, яка відповідає декільком протоколам, ми можемо використовувати protocolдекларатив:

var object: protocol<ProtocolOne,ProtocolTwo>//etc

Що робити, якщо я хотів би оголосити об’єкт, який відповідає одному або декільком протоколам, а також має певний тип базового класу? Еквівалент Objective-C буде виглядати так:

NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;

У Swift я би очікував, що це виглядатиме так:

var object: TYPE_NAME,ProtocolOne//etc

Це дає нам гнучкість можливості мати справу з реалізацією базового типу, а також доданим інтерфейсом, визначеним у протоколі.

Чи є інший більш очевидний спосіб, що я можу пропасти безвісти?

Приклад

Як приклад, скажімо, у мене є UITableViewCellзавод, який відповідає за повернення комірок, що відповідають протоколу. Ми можемо легко налаштувати загальну функцію, яка повертає клітинки, що відповідають протоколу:

class CellFactory {
    class func createCellForItem<T: UITableViewCell where T:MyProtocol >(item: SpecialItem,tableView: UITableView) -> T {
        //etc
    }
}

пізніше я хочу зняти з комірок ці комірки, використовуючи як тип, так і протокол

var cell: MyProtocol = CellFactory.createCellForItem(somethingAtIndexPath) as UITableViewCell

Це повертає помилку, оскільки комірка подання таблиці не відповідає протоколу ...

Я хотів би мати можливість вказати, що комірка є a UITableViewCellі відповідає MyProtocolоголошенню змінної?

Виправдання

Якщо ви знайомі з Factory Pattern, це має сенс у контексті можливості повернення об'єктів певного класу, що реалізують певний інтерфейс.

Як і в моєму прикладі, іноді ми любимо визначати інтерфейси, які мають сенс у застосуванні до певного об'єкта. Мій приклад комірки подання таблиці - одне з таких обґрунтувань.

Незважаючи на те, що наданий тип не зовсім відповідає згаданому інтерфейсу, об'єкт, який повертає завод, відповідає, і тому я хотів би мати гнучкість у взаємодії як з базовим типом класу, так і з оголошеним інтерфейсом протоколу


Вибачте, але який сенс у цьому швидко. Типи вже знають, яким протоколам вони відповідають. Що не просто використовувати тип?
Кірштейнс

1
@Kirsteins Не, якщо тип не повертається із заводу і, отже, є загальним типом із загальним базовим класом
Даніель Галаско,

Будь ласка, наведіть приклад, якщо це можливо.
Кірштайнс

NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;. Цей об’єкт здається абсолютно марним, оскільки NSSomethingвже знає, з чим він відповідає. Якщо це не відповідає одному з протоколів у <>вас, ви отримаєте unrecognised selector ...збої. Це взагалі не забезпечує безпеку типу.
Кірштейнс

@Kirsteins Будь ласка, ще раз перегляньте мій приклад, він використовується, коли ви знаєте, що об'єкт, який продає ваша фабрика, має певний базовий клас, що відповідає визначеному протоколу
Даніель Галаско,

Відповіді:


72

У Swift 4 тепер можна оголосити змінну, яка є підкласом типу і реалізує один або кілька протоколів одночасно.

var myVariable: MyClass & MyProtocol & MySecondProtocol

Щоб зробити необов’язкову змінну:

var myVariable: (MyClass & MyProtocol & MySecondProtocol)?

або як параметр методу:

func shakeEm(controls: [UIControl & Shakeable]) {}

Apple оголосила про це на WWDC 2017 на сесії 402: Що нового в Swift

По-друге, я хочу поговорити про складання класів та протоколів. Отже, тут я представив цей хиткий протокол для елемента інтерфейсу, який може дати невеликий ефект струсу, щоб привернути увагу до себе. І я продовжив і розширив деякі класи UIKit, щоб насправді забезпечити цю функцію струшування. А тепер я хочу написати щось, що здається простим. Я просто хочу написати функцію, яка приймає купу елементів управління, які є непохитними, і струшує ті, які мають можливість привертати до них увагу. Який тип я можу написати тут у цьому масиві? Це насправді неприємно і складно. Отже, я міг би спробувати використовувати елемент керування інтерфейсом. Але в цій грі не всі елементи керування інтерфейсом важко похитнути. Я міг би спробувати шаткий, але не всі шейкабельні елементи керування інтерфейсом. І насправді немає хорошого способу представити це в Swift 3.Swift 4 вводить поняття складання класу з будь-якою кількістю протоколів.


3
Просто додаю посилання на швидку еволюційну пропозицію github.com/apple/swift-evolution/blob/master/proposals/…
Даніель Галаско,

Дякую Філіпе!
Омар Альбейк,

що, якщо потрібна необов’язкова змінна цього типу?
Вячаслав Герчиков

2
@VyachaslavGerchicov: Ви можете поставити дужки навколо нього, а потім знак запитання такий: var myVariable: (MyClass & MyProtocol & MySecondProtocol)?
Філіп Отто

30

Ви не можете оголосити змінну як

var object:Base,protocol<ProtocolOne,ProtocolTwo> = ...

ні оголошувати тип повернення функції як

func someFunc() -> Base,protocol<MyProtocol,Protocol2> { ... }

Ви можете оголосити такий параметр функції, як цей, але в основному це оновлення.

func someFunc<T:Base where T:protocol<MyProtocol1,MyProtocol2>>(val:T) {
    // here, `val` is guaranteed to be `Base` and conforms `MyProtocol` and `MyProtocol2`
}

class SubClass:BaseClass, MyProtocol1, MyProtocol2 {
   //...
}

let val = SubClass()
someFunc(val)

Наразі все, що ви можете зробити, це:

class CellFactory {
    class func createCellForItem(item: SpecialItem) -> UITableViewCell {
        return ... // any UITableViewCell subclass
    }
}

let cell = CellFactory.createCellForItem(special)
if let asProtocol = cell as? protocol<MyProtocol1,MyProtocol2> {
    asProtocol.protocolMethod()
    cell.cellMethod()
}

З цим технічно cellідентично asProtocol.

Але, що стосується компілятора, cellмає UITableViewCellлише інтерфейс , тоді як asProtocolмає інтерфейс лише протоколів. Отже, коли ви хочете викликати UITableViewCellметоди '', ви повинні використовувати cellзмінну. Коли ви хочете викликати метод протоколів, використовуйте asProtocolзмінну.

Якщо ви впевнені, що комірка відповідає протоколам, вам не доведеться користуватися if let ... as? ... {}. подібно до:

let cell = CellFactory.createCellForItem(special)
let asProtocol = cell as protocol<MyProtocol1,MyProtocol2>

Оскільки фабрика визначає типи повернення, мені технічно не потрібно виконувати додатковий привід? Я міг би просто розраховувати на швидкі неявні набори тексту, щоб виконати набір тексту там, де явно оголошую протоколи?
Даніель Галаско,

Я не розумію, що ти маєш на увазі, вибач за мої погані знання англійської мови. Якщо ви говорите про -> UITableViewCell<MyProtocol>, це недійсно, оскільки UITableViewCellце не загальний тип. Я думаю, це навіть не компілюється.
rintaro

Я маю на увазі не вашу загальну реалізацію, а скоріше ваш приклад ілюстрації реалізації. де ви говорите нехай asProtocol = ...
Даніель Галаско

або я міг просто зробити: var cell: protocol <ProtocolOne, ProtocolTwo> = someObject як UITableViewCell і отримати переваги обох в одній змінній
Даніель Галаско,

2
Я не думаю, що так. Навіть якщо ви могли б зробити це, cellмає лише методи протоколів (для компілятора).
rintaro

2

На жаль, Swift не підтримує відповідність протоколу об’єктного рівня. Однак є дещо незручне навколо, яке може служити вашим цілям.

struct VCWithSomeProtocol {
    let protocol: SomeProtocol
    let viewController: UIViewController

    init<T: UIViewController>(vc: T) where T: SomeProtocol {
        self.protocol = vc
        self.viewController = vc
    }
}

Потім, де б вам не потрібно було робити що-небудь, що має UIViewController, ви отримаєте доступ до аспекту .viewController структури та до всього, що вам потрібно, аспект протоколу, ви б посилалися на .protocol.

Наприклад:

class SomeClass {
   let mySpecialViewController: VCWithSomeProtocol

   init<T: UIViewController>(injectedViewController: T) where T: SomeProtocol {
       self.mySpecialViewController = VCWithSomeProtocol(vc: injectedViewController)
   }
}

Тепер у будь-який час, коли вам потрібен mySpecialViewController, щоб робити щось, що стосується UIViewController, ви просто посилаєтесь на mySpecialViewController.viewController, і коли вам потрібно, щоб він виконував якусь функцію протоколу, ви посилаєтесь на mySpecialViewController.protocol.

Сподіваємось, Swift 4 дозволить нам оголосити об’єкт із приєднаними до нього протоколами в майбутньому. Але наразі це працює.

Сподіваюся, це допомагає!


1

EDIT: Я помилився , але якщо хтось ще прочитав це непорозуміння, як я, я залишаю цю відповідь там. OP запитав про перевірку відповідності протоколу об'єкта даного підкласу, і це вже інша історія, як показує прийнята відповідь. Ця відповідь говорить про відповідність протоколу базового класу.

Можливо, я помиляюся, але ви не говорите про додавання відповідності протоколу до UITableCellViewкласу? У цьому випадку протокол поширюється на базовий клас, а не на об'єкт. Дивіться документацію Apple щодо декларування прийняття протоколу з розширенням, яке у вашому випадку буде приблизно таким:

extension UITableCellView : ProtocolOne {}

// Or alternatively if you need to add a method, protocolMethod()
extension UITableCellView : ProcotolTwo {
   func protocolTwoMethod() -> String {
     return "Compliant method"
   }
}

На додаток до вже вказаної документації Swift, також дивіться статтю Nate Cooks Загальні функції для несумісних типів з подальшими прикладами.

Це дає нам гнучкість можливості мати справу з реалізацією базового типу, а також доданим інтерфейсом, визначеним у протоколі.

Чи є інший більш очевидний спосіб, що я можу пропасти безвісти?

Прийняття протоколу зробить саме це, змусивши об’єкт дотримуватися заданого протоколу. Однак пам’ятайте про негативну сторону, що змінна даного типу протоколу не знає нічого поза протоколом. Але цього можна обійти, визначивши протокол, який має всі необхідні методи / змінні / ...

Незважаючи на те, що наданий тип не зовсім відповідає згаданому інтерфейсу, об'єкт, який повертає завод, відповідає, і тому я хотів би мати гнучкість у взаємодії як з базовим типом класу, так і з оголошеним інтерфейсом протоколу

Якщо ви хочете, щоб загальний метод, змінний відповідав як протоколам, так і базовим класам, вам може не пощастити. Але схоже на те, що вам потрібно визначити протокол досить широким, щоб мати необхідні методи відповідності, і в той же час досить вузьким, щоб мати можливість застосувати його до базових класів без зайвої роботи (тобто просто заявляючи, що клас відповідає протокол).


1
Це зовсім не те, про що я говорю, але дякую :) Я хотів мати можливість взаємодіяти з об’єктом як за допомогою його класу, так і за допомогою певного протоколу. Подібно до того, як в obj-c я можу робити NSObject <MyProtocol> obj = ... Само собою зрозуміло, це не можна зробити швидко, вам потрібно передати об'єкт за його протоколом
Даніель Галаско

0

Одного разу у мене була подібна ситуація при спробі зв’язати мої загальні зв’язки взаємодію в Storyboards (IB не дозволяє підключати розетки до протоколів, лише екземпляри об’єктів), яку я обійшов, просто маскуючи публічний ivar базового класу з приватним обчислювальним майно. Хоча це не заважає комусь робити незаконне призначення як таке, воно забезпечує зручний спосіб безпечно запобігти будь-якій небажаній взаємодії з невідповідним екземпляром під час виконання. (тобто запобігти виклику методів делегатів до об’єктів, які не відповідають протоколу.)

Приклад:

@objc protocol SomeInteractorInputProtocol {
    func getSomeString()
}

@objc protocol SomeInteractorOutputProtocol {
    optional func receiveSomeString(value:String)
}

@objc class SomeInteractor: NSObject, SomeInteractorInputProtocol {

    @IBOutlet var outputReceiver : AnyObject? = nil

    private var protocolOutputReceiver : SomeInteractorOutputProtocol? {
        get { return self.outputReceiver as? SomeInteractorOutputProtocol }
    }

    func getSomeString() {
        let aString = "This is some string."
        self.protocolOutputReceiver?.receiveSomeString?(aString)
    }
}

"OutputReceiver" оголошено необов'язковим, як і приватний "protocolOutputReceiver". Завжди отримуючи доступ до outputReceiver (він же делегат) через останню (обчислювану властивість), я ефективно фільтрую будь-які об'єкти, які не відповідають протоколу. Тепер я можу просто використовувати необов’язковий ланцюжок, щоб безпечно викликати об’єкт-делегат незалежно від того, реалізує він протокол чи навіть існує.

Щоб застосувати це до вашої ситуації, ви можете мати загальнодоступний ivar типу "YourBaseClass?" (на відміну від AnyObject) і використовуйте приватне обчислюване властивість для забезпечення відповідності протоколу. FWIW.

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