Как в 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в объявлении переменной?

Обоснование

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

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

Хотя предоставленный тип не совсем соответствует упомянутому интерфейсу, объект, возвращаемый фабрикой, соответствует, и поэтому мне хотелось бы гибкости во взаимодействии как с типом базового класса, так и с объявленным интерфейсом протокола.

Даниэль Галаско
источник
Извините, но какой в ​​этом смысл в быстром. Типы уже знают, каким протоколам они соответствуют. Что не просто использовать тип?
Кирстейнс
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>
Ринтаро
источник
Поскольку фабрика определяет возвращаемые типы, мне технически не нужно выполнять необязательное приведение? Я мог бы просто положиться на неявную типизацию Swifts, чтобы выполнить типизацию, в которой я явно объявляю протоколы?
Даниэль Галаско
Я не понимаю, что вы имеете в виду, извините за мои плохие знания английского языка. Если вы говорите о -> UITableViewCell<MyProtocol>, это неверно, потому что UITableViewCellэто не общий тип. Я думаю, это даже не компилируется.
rintaro
Я не имею в виду вашу общую реализацию, а скорее примерную иллюстрацию реализации. где вы говорите let asProtocol = ...
Даниэль Галаско
или я мог бы просто сделать: var cell: protocol <ProtocolOne, ProtocolTwo> = someObject as 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

РЕДАКТИРОВАТЬ: Я ошибался , но если кто-то еще прочитал это недоразумение, как я, я оставлю этот ответ там. 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 см. Также статью Нейта Кука « Общие функции для несовместимых типов» с дополнительными примерами.

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

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

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

Хотя предоставленный тип не совсем соответствует упомянутому интерфейсу, объект, возвращаемый фабрикой, соответствует, и поэтому мне хотелось бы гибкости во взаимодействии как с типом базового класса, так и с объявленным интерфейсом протокола.

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

Холрой
источник
1
Я говорю совсем не об этом, но спасибо :) Я хотел иметь возможность взаимодействовать с объектом как через его класс, так и через определенный протокол. Точно так же, как в obj-c я могу сделать NSObject <MyProtocol> obj = ... Само собой разумеется, что это невозможно сделать быстро, вы должны
передать
0

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

тимьян
источник