Swift: соответствие протоколу с общим методом с предложением «где»

Резюме:

Я хотел бы создать 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
    }    
}

Type 'AnotherClientOfTableController' does not conform to protocol 'GenericTableControllerDelegate' Do you want to add protocol stubs?

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

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

Swift: соответствие протоколу с общим методом с предложением «где»

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
0
898
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

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")
    }

}

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

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. Ничто не останавливает компилятор, но если бы это было так, в чем была бы ошибка?

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

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

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, которая часто имеет некоторые проблемы при работе с дженериками и протоколами.

Я определенно согласен с использованием здесь обратного вызова, а не делегата, но передача такого метода часто создает цикл удержания и требует большой осторожности. vc.onSelect = handleSelection неявно фиксирует self как сильную ссылку. В таких случаях вам часто придется использовать [weak self]. В данном конкретном случае это не так, потому что vc — локальная переменная, но я подозреваю, что в любом практическом коде vc будет свойством.

Rob Napier 31.05.2019 14:39

@RobNapier Согласен, хорошая мысль. Не могли бы вы улучшить эту идею, чтобы я мог использовать как именованный метод (не анонимный), так и заставить контроллер CallbackExample слабо его захватывать? Так что, если я сохраню ссылку на него где-нибудь в `CallBackClient, он все равно будет освобожден.

Richard Topchii 31.05.2019 14:58

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

Richard Topchii 31.05.2019 14:59

Я не верю, что такой синтаксис существует, но я добавил ответ, который расширяет это. Как вы говорите, функции, безусловно, дорога ИМО.

Rob Napier 31.05.2019 15:53
Ответ принят как подходящий

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

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)
}

А затем вы хотели, чтобы тип данных делегата соответствовал типу данных табличного представления. И это приводит нас в мир 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
}

При этом вы можете создавать простые стратегии, такие как «распечатайте это:»

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.

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

Richard Topchii 03.06.2019 10:26

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

Richard Topchii 03.06.2019 10:29

Другие вопросы по теме