В Swift, как я могу объявить переменную определенного типа, которая соответствует одному или нескольким протоколам?

В 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 factory, который отвечает за возврат ячеек, соответствующих протоколу. Мы можем легко настроить общую функцию, которая возвращает ячейки, соответствующие протоколу:

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

позже я хочу удалить эти ячейки, используя как тип, так и протокол

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

Это возвращает ошибку, поскольку ячейка представления таблицы не соответствует протоколу...

Я хотел бы указать, что ячейка является UITableViewCell и соответствует MyProtocol в объявлении переменной?

Оправдание

Если вы знакомы с шаблоном Factory, это будет иметь смысл в контексте возможности возврата объектов определенного класса, реализующего определенный интерфейс.

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

Пока поставляемый тип точно не соответствует указанному интерфейсу, возвращается объект factory, поэтому мне нужна гибкость при взаимодействии как с типом базового класса, так и с объявленным интерфейсом протокола

Ответ 1

В Swift 4 теперь можно объявить переменную, которая является подклассом типа и одновременно реализует один или несколько протоколов.

var myVariable: MyClass & MyProtocol & MySecondProtocol

или как параметр метода:

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

Apple объявила об этом на WWDC 2017 в Сессия 402: Что нового в Swift

Во-вторых, я хочу поговорить о создании классов и протоколов. Так вот Я представил этот шаткий протокол для элемента пользовательского интерфейса, который может дать немного встряхнуть, чтобы привлечь внимание к себе. И я пошел вперед и расширили некоторые классы UIKit, чтобы фактически обеспечить это встряхивание функциональность. И теперь я хочу написать что-то, что кажется простым. я просто хочу написать функцию, которая принимает кучу элементов управления, которые дрожащие и трясущиеся те, которые позволяют привлечь внимание к их. Какой тип я могу написать здесь в этом массиве? Это на самом деле разочарование и сложность. Итак, я могу попытаться использовать элемент управления пользовательского интерфейса. Но нет все элементы управления пользовательским интерфейсом являются шаткой в ​​этой игре. Я мог бы попытаться поколебать, но не все shakables являются элементами управления пользовательского интерфейса. И на самом деле нет хорошего способа представляют это в Swift 3. Swift 4 вводит понятие составления класс с любым количеством протоколов.

Ответ 2

Вы не можете объявить переменную как

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>

Ответ 3

К сожалению, 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 позволит нам объявить объект с подключенными к нему протоколами в будущем. Но пока это работает.

Надеюсь, это поможет!

Ответ 4

EDIT: Я ошибся, но если кто-то еще прочитает это недоразумение, как я, я оставляю этот ответ там. ОП попросил проверить соответствие протокола объекту данного подкласса, а это еще одна история, как показывает принятый ответ. Этот ответ говорит о соответствии протокола для базового класса.

Возможно, я ошибаюсь, но разве вы не говорите о добавлении соответствия протокола классу 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 Общие функции для несовместимых типов с дополнительными примерами.

Это дает нам гибкость в том, чтобы иметь дело с реализацией базового типа, а также с добавленным интерфейсом, определенным в протоколе.

Есть ли еще более очевидный способ, которым я мог бы не хватать?

Принятие протокола будет делать именно это, сделать объект привязанным к данному протоколу. Однако следует помнить о неблагоприятной стороне, что переменная данного типа протокола ничего не знает вне протокола. Но это можно обойти, указав протокол, который имеет все необходимые методы/переменные/...

Пока поставляемый тип точно не соответствует указанному интерфейсу, возвращается объект factory, поэтому мне нужна гибкость при взаимодействии как с типом базового класса, так и с объявленным интерфейсом протокола

Если вы хотите, чтобы для универсального метода переменная соответствовала как типу протокола, так и базовому классу, вам может быть не повезло. Но похоже, что вам необходимо определить протокол достаточно широко, чтобы иметь необходимые методы соответствия и в то же время достаточно узкие, чтобы иметь возможность использовать его для базовых классов без большой работы (т.е. Просто объявить, что класс соответствует протокол).

Ответ 5

У меня когда-то была аналогичная ситуация при попытке связать мои общие подключения для взаимодействия в Storyboards (IB не позволит вам подключать выходы к протоколам, только экземпляры объектов), которые я обошел, просто маскируя базовый класс public 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 (делегат a.k.a.) через последнее (вычисленное свойство), я эффективно отфильтровываю любые объекты, которые не соответствуют протоколу. Теперь я могу просто использовать необязательную цепочку для безопасного вызова объекту делегата независимо от того, реализует ли он протокол или даже существует.

Чтобы применить это к вашей ситуации, вы можете иметь общедоступный ivar типа "YourBaseClass"? (в отличие от AnyObject) и использовать частное вычисляемое свойство для обеспечения соответствия протокола. FWIW.