Резюме:
Я хотел бы создать 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?
Есть ли способ иметь протокол с универсальным методом и иметь конкретный (специализированный) тип в реализации этого метода?
Существуют ли близкие альтернативы, удовлетворяющие аналогичному требованию (имеющие универсальный класс, но способные обрабатывать конкретный тип в обратном вызове делегата)?
Я не думаю, что это выполнимо в том смысле, в каком вы этого хотите. Наиболее близким было бы объединение с подклассом. Рассмотрим следующее:
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, которая часто имеет некоторые проблемы при работе с дженериками и протоколами.
@RobNapier Согласен, хорошая мысль. Не могли бы вы улучшить эту идею, чтобы я мог использовать как именованный метод (не анонимный), так и заставить контроллер CallbackExample
слабо его захватывать? Так что, если я сохраню ссылку на него где-нибудь в `CallBackClient, он все равно будет освобожден.
Я только что проверил на простом демонстрационном проекте: действительно, при сохранении строгой ссылки на недавно созданный контроллер представления сохранение обратного вызова также создает цикл сохранения.
Я не верю, что такой синтаксис существует, но я добавил ответ, который расширяет это. Как вы говорите, функции, безусловно, дорога ИМО.
Ваша ошибка в протоколе:
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.
Привет, Роб, спасибо за обширный обзор дженериков и экзистенциалов в вашей статье и примеры кода, а также пример инкапсуляции команды/стратегии. Выглядит многообещающе, но слишком много для варианта использования, который я хотел бы иметь. Одна из целей, которые я имею в виду, состоит в том, чтобы иметь чистый синтаксис на месте вызова и не иметь проблем с управлением памятью.
Подход Ричарда Топчий хорош, однако он терпит неудачу, когда дело доходит до сильного захвата self
(т.е. вызывающего объекта) и может создавать циклы удержания. Ваша идея с setDelegate
определенно интересна. Кажется, что Swift еще не поддерживает выражение этой связи кратким и простым в использовании способом, поэтому я мог бы использовать ваш или какой-либо другой подход для инкапсуляции универсального селектора.
Я определенно согласен с использованием здесь обратного вызова, а не делегата, но передача такого метода часто создает цикл удержания и требует большой осторожности.
vc.onSelect = handleSelection
неявно фиксируетself
как сильную ссылку. В таких случаях вам часто придется использовать[weak self]
. В данном конкретном случае это не так, потому чтоvc
— локальная переменная, но я подозреваю, что в любом практическом кодеvc
будет свойством.