Swift: соответствие протоколу с общим методом с предложением «где» - PullRequest
4 голосов
/ 31 мая 2019

Сводка:

Я хотел бы создать Class<T>, который бы имел соответствующий ClassDelegate протокол с func<T> в нем.

Цель:

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

Пример кода:

Протокол суниверсальный метод:

protocol GenericTableControllerDelegate: AnyObject {
    func controller<T>(controller: GenericTableController<T>, didSelect value: T)
}

универсальная база UITableViewController подкласс:

open class GenericTableController<DataType>: UITableViewController {
    weak var delegate: GenericTableControllerDelegate?
    var data = [DataType]()

    open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = data[indexPath.row]
        delegate?.controller(controller: self, didSelect: item)
    }
}

специализированная версия GenericTableController:

final class SpecializedTableController: GenericTableController<NSObject> {}

клиентSpecializedTableController - достигает результата, но требует приведения типов для доступа к специализированному типу данных:

final class ClientOfTableController: UIViewController, GenericTableControllerDelegate {
    // Works OK
    func controller<T>(controller: GenericTableController<T>, didSelect value: T) {
        if let value = value as? NSObject {
            // Requires unwrapping and casting
        }
    }
}

Клиент SpecializedTableController, с требованием «где» - единственная проблема, котораяон не компилируется

final class AnotherClientOfTableController: UIViewController, GenericTableControllerDelegate {
    // Works OK
    func controller<T>(controller: GenericTableController<T>, didSelect value: T) where T: NSObject {
        // `value` is String
    }    
}

Тип 'AnotherClientOfTableController' не соответствует протоколу 'GenericTableControllerDelegate' Хотите добавить заглушки протокола?

Есть ли способ иметь протокол с универсальным методом и иметь возможность иметь конкретный (специализированный) тип в реализации этого метода?

Существуют ли близкие альтернативы, достигающие удовлетворения аналогичного требования (имеяуниверсальный класс, но способный обрабатывать конкретный тип в обратном вызове делегата)?

screenshot

Ответы [ 3 ]

3 голосов
/ 31 мая 2019

Ваша ошибка в протоколе:

protocol GenericTableControllerDelegate: AnyObject {
    func controller<T>(controller: GenericTableController<T>, didSelect value: T)
}

Это говорит о том, что для того, чтобы быть GTCD, тип должен принять любой тип T, переданный этой функции.Но вы не это имеете в виду.Вы имели в виду следующее:

public protocol GenericTableControllerDelegate: AnyObject {
    associatedtype DataType
    func controller(controller: GenericTableController<DataType>, didSelect value: DataType)
}

А затем вы хотели, чтобы DataType делегата соответствовал DataType табличного представления.И это погружает нас в мир PAT (протоколов со связанными типами), стирающих типов и обобщенных экзистенциалов (которых еще нет в Swift), и на самом деле это просто беспорядок.

Несмотря на то, что это вариант использования, для которого обобщенные экзистенциалы особенно хорошо подходят (если они когда-либо добавляются в Swift), во многих случаях вы, вероятно, вообще не хотите этого.Шаблон делегирования является шаблоном ObjC, разработанным до добавления замыканий.Раньше было очень трудно передавать функции в ObjC, поэтому даже очень простые обратные вызовы превращались в делегатов.В большинстве случаев, я думаю, что подход Ричарда Топчия абсолютно правильный.Просто передайте функцию.

Но что, если вы действительно хотите сохранить стиль делегата?Мы можем (почти) сделать это.Единственный недостаток в том, что вы не можете иметь свойство с именем delegate.Вы можете установить его, но не можете получить его.

open class GenericTableController<DataType>: UITableViewController
{
    // This is the function to actually call
    private var didSelect: ((DataType) -> Void)?

    // We can set the delegate using any implementer of the protocol
    // But it has to be called `controller.setDelegate(self)`.
    public func setDelegate<Delegate: GenericTableControllerDelegate>(_ d: Delegate?)
        where Delegate.DataType == DataType {
            if let d = d {
                didSelect = { [weak d, weak self] in
                    if let self = self { d?.controller(controller: self, didSelect: $0) }
                }
            } else {
                didSelect = nil
            }
    }

    var data = [DataType]()

    // and here, just call our internal method
    open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = data[indexPath.row]
        didSelect?(item)
    }
}

Это полезный метод для понимания, но я, вероятно, не буду использовать его в большинстве случаев.Определенно возникает головная боль, когда вы добавляете больше методов, если эти методы ссылаются на DataType.Вам нужно много шаблонов.Обратите внимание, что из-за передачи self методу делегата возникает некоторая путаница.Это то, что нужно методам делегата, но замыкания не нужны (вы всегда можете захватить контроллер в замыкании, если оно нужно).

По мере того, как вы исследуете такой код многократного использования, я призываю вас больше думать об инкапсуляциистратегии, а не об объектах и ​​протоколах делегатов.Примером инкапсуляции стратегии может служить тип SelectionHandler, который вы передаете контроллеру:

struct SelectionHandler<Element> {
    let didSelect: (Element) -> Void
}

С этим вы можете создавать простые стратегии, такие как «print it out»:

extension SelectionHandler {
    static func printSelection() -> SelectionHandler {
        return SelectionHandler { print($0) }
    }
}

Или, что более интересно, обновите метку:

static func update(label: UILabel) -> SelectionHandler {
    return SelectionHandler { [weak label] in label?.text = "\($0)" }
}

Итак, вы получите код вроде:

controller.selectionHandler = .update(label: self.nameLabel)

Или, что еще более интересно, вы можете создавать типы более высокого порядка:

static func combine(_ handlers: [SelectionHandler]) -> SelectionHandler {
    return SelectionHandler {
        for handler in handlers {
            handler.didSelect($0)
        }
    }
}

static func trace(_ handler: SelectionHandler) -> SelectionHandler {
    return .combine([.printSelection(), handler])
}

controller.selectionHandler = .trace(.update(label: self.nameLabel))

Этот подход сложнее, чем делегирование, и начинает раскрывать реальные преимущества Swift.

1 голос
/ 31 мая 2019

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

open class GenericTableController2<DataType>: UITableViewController {
    var onSelect: ((DataType) -> Void)?
    var data = [DataType]()

    open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = data[indexPath.row]
        onSelect?(item)
    }
}

final class CallbackExample: GenericTableController2<NSObject> {
}

final class CallBackClient: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let vc = CallbackExample()
        vc.onSelect = handleSelection
    }

    func handleSelection(_ object: NSObject) {

    }
}

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

0 голосов
/ 31 мая 2019

Я не думаю, что это выполнимо в том смысле, что вы этого хотите. Наиболее близким будет объединение с подклассом. Учтите следующее:

protocol MagicProtocol {
    func dooMagic<T>(_ trick: T)
}

class Magician<TrickType> {
    private let listener: MagicProtocol
    private let tricks: [TrickType]
    init(listener: MagicProtocol, tricks: [TrickType]) { self.listener = listener; self.tricks = tricks }
    func abracadabra() { listener.dooMagic(tricks.randomElement()) }
}

class Audience<DataType>: MagicProtocol {

    var magician: Magician<DataType>?

    init() {
        magician?.abracadabra()
    }

    func doExplicitMagic(_ trick: DataType) {

    }

    func dooMagic<T>(_ trick: T) {
        doExplicitMagic(trick as! DataType)
    }

}

Теперь я могу создать подкласс и ограничить его некоторым типом:

class IntegerAudience: Audience<Int> {

    override func doExplicitMagic(_ trick: Int) {
        print("This works")
    }

}

Проблема в том, что нет никакой корреляции между этими 2 дженериками. Так что в какой-то момент актерский состав должен быть сделан. Здесь мы делаем это в методе протокола:

doExplicitMagic(trick as! DataType)

кажется, что это довольно безопасно и никогда не может произойти сбой, но если вы посмотрите немного ближе, мы могли бы сделать это:

func makeThingsGoWrong() {
    let myAudience = IntegerAudience()
    let evilMagician = Magician(listener: myAudience, tricks: ["Time to burn"])
    evilMagician.abracadabra() // This should crash the app
}

Здесь myAudience соответствует протоколу MagicProtocol, который не может быть ограничен универсальным. Но myAudience ограничен Int. Ничто не останавливает компилятор, но если это произойдет, какая будет ошибка?

Во всяком случае, это работает, если вы используете его правильно. Если вы этого не сделаете, то он рухнет. Вы можете сделать дополнительную распаковку, но я не уверен, что это уместно.

...