Как указать тип аргумента в сигнатуре закрытия в Swift?

Я пытаюсь написать класс легкого наблюдателя в Swift (в настоящее время Swift 2). Идея состоит в том, чтобы использовать его в системе Entity Component как средство взаимодействия компонентов друг с другом, не будучи связанными друг с другом.

Проблема, с которой я сталкиваюсь, заключается в том, что могут быть переданы все типы данных, CGVector, NSTimeInterval и так далее. Это означает, что передаваемый метод может иметь всевозможные сигнатуры типов (CGVector) -> Void, () -> Void и т. д.

Я хотел бы иметь возможность хранить эти различные подписи в массиве, но при этом иметь некоторую безопасность типов. Я думаю, что тип массива должен быть (Any) -> Void или, возможно, (Any?) -> Void, так что я могу, по крайней мере, убедиться, что он содержит методы. Но у меня проблемы с передачей методов таким образом: Cannot convert value of type '(CGVector) -> ()' to expected argument type '(Any) -> ()'.

Первая попытка:

//: Playground - noun: a place where people can play

import Cocoa
import Foundation

enum EventName: String {
    case input, update
}

struct Binding{
    let listener: Component
    let action: (Any) -> ()
}

class EventManager {
    var events = [EventName: [Binding]]()

    func add(name: EventName, event: Binding) {
        if var eventArray = events[name] {
            eventArray.append(event)
        } else {
            events[name] = [event]
        }
    }

    func dispatch(name: EventName, argument: Any) {
        if let eventArray = events[name] {
            for element in eventArray {
                element.action(argument)
            }
        }
    }

    func remove(name: EventName, listener: Component) {
        if var eventArray = events[name] {
            eventArray = eventArray.filter(){ $0.listener.doc  != listener.doc }
        }
    }
}

// Usage test

//Components

protocol Component {
    var doc: String { get }
}

class Input: Component {
    let doc = "InputComponent"
    let eventManager: EventManager

    init(eventManager: EventManager) {
        self.eventManager = eventManager
    }

    func goRight() {
        eventManager.dispatch(.input, argument: CGVector(dx: 10, dy: 0) )
    }
}

class Movement: Component {
    let doc = "MovementComponent"

    func move(vector: CGVector) {
        print("moved \(vector)")
    }

}

class Physics: Component {
    let doc = "PhysicsComponent"

    func update(time: NSTimeInterval){
        print("updated at \(time)")
    }
}


class someClass {
    //events
    let eventManager = EventManager()

    // components
    let inputComponent: Input
    let moveComponent = Movement()

    init() {
        inputComponent = Input(eventManager: eventManager)

        let inputBinding = Binding(listener: moveComponent, action: moveComponent.move) // ! Cannot convert value of type '(CGVector) -> ()' to expected argument type '(Any) -> ()'
        eventManager.add(.input, event: inputBinding)

    }
}

let someInstance = someClass()
someInstance.inputComponent.goRight()

Выдает ошибку Cannot convert value of type '(CGVector) -> ()' to expected argument type '(Any) -> ()'.

Вторая попытка

Если я обобщу структуру Binding для распознавания различных типов аргументов, мне повезет немного больше. Эта версия в основном работает, но массив, содержащий методы, теперь [Any] (я не уверен, что это попытка привести Any обратно к структуре Binding, которая вызывает немного странную ошибку ниже Binary operator '!=' cannot be applied to two 'String' operands):

struct Binding<Argument>{
    let listener: Component
    let action: (Argument) -> ()
}

class EventManager {
    var events = [EventName: [Any]]()

    func add(name: EventName, event: Any) {
        if var eventArray = events[name] {
            eventArray.append(event)
        } else {
            events[name] = [event]
        }
    }

    func dispatch<Argument>(name: EventName, argument: Argument) {
        if let eventArray = events[name] {
            for element in eventArray {
                (element as! Binding<Argument>).action(argument)
            }
        }
    }

    func remove(name: EventName, listener: Component) {
        if var eventArray = events[name] {
           // eventArray = eventArray.filter(){ ($0 as! Binding).listener.doc  != listener.doc } //Binary operator '!=' cannot be applied to two 'String' operands
        }
    }
}

Есть ли способ сделать это и заставить массив хранить методы сигнатур различного типа, что-то вроде [(Any?) -> ()] ?

Попытка 3...

Почитайте, например, здесь http://www.klundberg.com/blog/capturing-objects-weakly-in-instance-method-references-in-swift/ кажется, что мой подход, описанный выше, приведет к сильным циклам ссылок, и это то, что мне нужно нужно передать статический метод, например, Movement.move, а не moveComponent.move. Таким образом, сигнатура типа, которую я буду хранить, на самом деле будет (Component) -> (Any?) -> Void, а не (Any?) -> Void. Но мой вопрос остается в силе, я все еще хотел бы иметь возможность хранить массив этих статических методов с немного большей безопасностью типов, чем просто [Any].


person OliverD    schedule 28.06.2016    source источник
comment
Вы должны взглянуть на это. Может помочь вам.   -  person Dershowitz123    schedule 28.06.2016
comment
Напоминает мне эту реализацию ObserverSet, которая описана здесь: mikeash.com/pyblog/   -  person Casey Fleser    schedule 28.06.2016
comment
Основная проблема здесь в том, что вы не можете преобразовать (CGVector) -> () в (Any) -> (). Вы говорите, что заданная функция, которая принимает входные данные CGVector, теперь может принимать любые входные данные, что совершенно неверно (функции в этом отношении контравариантны). Я подозреваю, что вам придется либо исключить логику хранения замыканий, либо позволить тому, кто вызывает метод dispatch, предоставить замыкание для вызова, примерно так: проект github - или, как вы говорите, вам придется хранить замыкания как Any, а затем приводить их обратно, когда вам нужно их вызывать.   -  person Hamish    schedule 28.06.2016
comment
Спасибо за эти ссылки, очень интересно увидеть различные реализации такого шаблона наблюдателя/уведомителя. Мне особенно понравился подход Майка Эша. В нем у него есть решение/взлом, кажется, как указать параметр внутри сигнатуры закрытия, которая, кажется, частично повторяет его: f: { f($0 as T) }   -  person OliverD    schedule 28.06.2016
comment
@originaluser2 I suspect you'll either have to factor out the closure storage logic and let whoever calls the dispatch method provide the closure to call проблема заключается в том, что этот шаблон разделения зависит от того, что диспетчер совершенно не зависит от того, кто может или не может получать его отправки.   -  person OliverD    schedule 29.06.2016


Ответы (3)


Один из подходов к приведению параметров замыкания, предложенный в блоге Майка Эша, на который ссылается Кейси Флезер, состоит в том, чтобы "повторить" (?) его.

Универсальный класс Binding:

private class Binding<Argument>{
    weak var listener: AnyObject?
    let action: AnyObject -> Argument -> ()

    init(listener: AnyObject, action: AnyObject -> Argument -> ()) {
        self.listener = listener
        self.action = action
    }

    func invoke(data: Argument) -> () {
        if let this = listener {
            action(this)(data)
        }
    }
}

И менеджер событий, без повторения:

class EventManager {

    var events = [EventName: [AnyObject]]()

    func add<T: AnyObject, Argument>(name: EventName, listener: T, action: T -> Argument -> Void) {           
        let binding = Binding(listener: listener, action: action) //error: cannot convert value of type 'T -> Argument -> Void' to expected argument type 'AnyObject -> _ -> ()'

        if var eventArray = events[name] {
            eventArray.append(binding)
        } else {
            events[name] = [binding]
        }
    }

    func dispatch<Argument>(name: EventName, argument: Argument) {
        if let eventArray = events[name] {
            for element in eventArray {
                (element as! Binding<Argument>).invoke(argument)
            }
        }
    }

    func remove(name: EventName, listener: Component) {
        if var eventArray = events[name] {
            eventArray = eventArray.filter(){ $0 !== listener }
        }
    }
}

Это по-прежнему вызывает ту же ошибку, что невозможно выполнить приведение к AnyObject:

error: cannot convert value of type 'T -> Argument -> Void' to expected argument type 'AnyObject -> _ -> ()'.

Но если мы вызовем первую часть каррируемой функции и заключим ее в новое замыкание (я не знаю, есть ли у этого название, я называю это «повторяющимся»), например: action: { action($0 as! T) }, тогда все будет работать ( техника взята у Майка Эша). Я предполагаю, что это что-то вроде взлома, поскольку безопасность типа Swift обходится.

Я также не очень понимаю сообщение об ошибке: в нем говорится, что он не может преобразовать T в AnyObject, но затем принимает приведение к T?

РЕДАКТИРОВАТЬ: обновлен полный код до сих пор edit2: исправлено, как добавляются события edit3: удаление событий теперь работает

//: Playground - noun: a place where people can play

import Cocoa
import Foundation

enum EventName: String {
    case input, update
}

private class Binding<Argument>{
    weak var listener: AnyObject?
    let action: AnyObject -> Argument -> ()

    init(listener: AnyObject, action: AnyObject -> Argument -> ()) {
        self.listener = listener
        self.action = action
    }

    func invoke(data: Argument) -> () {
        if let this = listener {
            action(this)(data)
        }
    }

}


class EventManager {
    var events = [EventName: [AnyObject]]()

    func add<T: AnyObject, Argument>(name: EventName, listener: T, action: T -> Argument -> Void) {

        let binding = Binding(listener: listener, action: { action($0 as! T)  }) //

        if events[name]?.append(binding) == nil {
            events[name] = [binding]
        }
    }

    func dispatch<Argument>(name: EventName, argument: Argument) {
        if let eventArray = events[name] {
            for element in eventArray {
                (element as! Binding<Argument>).invoke(argument)
            }
        }
    }

    func remove<T: AnyObject, Argument>(name: EventName, listener: T, action: T -> Argument -> Void) {
        events[name]? = events[name]!.filter(){ ( $0 as! Binding<Argument>).listener !== listener }
    }
}

// Usage test

//Components

class Component {

    weak var events: EventManager?
    let doc: String
    init(doc: String){
        self.doc = doc
    }

}


class Input: Component {

    init() {
        super.init(doc: "InputComponent")
    }

    func goRight() {
        events?.dispatch(.input, argument: CGVector(dx: 10, dy: 0) )
    }

    func goUp() {
        events?.dispatch(.input, argument: CGVector(dx: 0, dy: -5) )
    }
}

class Movement: Component {
    init() {
        super.init(doc: "MovementComponent")
    }
    func move(vector: CGVector) {
        print("moved \(vector)")
    }

}


class Physics: Component {
    init() {
        super.init(doc: "PhysicsComponent")
    }

    func update(time: NSTimeInterval){
        print("updated at \(time)")
    }

    func move(vector: CGVector) {
        print("updated \(vector)")
    }

}

// Entity

class Entity {

    let events = EventManager()

}


class someClass: Entity {

    // components
    let inputComponent: Input
    let moveComponent: Movement
    let physicsComponent: Physics

    override init() {

        inputComponent = Input()
        moveComponent = Movement()
        physicsComponent = Physics()
        super.init()
        inputComponent.events = events

        events.add(.input, listener: moveComponent, action: Movement.move)
        events.add(.input, listener: physicsComponent, action: Physics.move)
    }
}

let someInstance = someClass()

someInstance.inputComponent.goRight()
//moved CGVector(dx: 10.0, dy: 0.0)
//updated CGVector(dx: 10.0, dy: 0.0)

someInstance.events.remove(.input, listener: someInstance.moveComponent, action: Movement.move)
someInstance.inputComponent.goUp()
//updated CGVector(dx: 0.0, dy: -5.0)

someInstance.events.remove(.input, listener: someInstance.physicsComponent, action: Physics.move)
someInstance.inputComponent.goRight()
// nothing
person OliverD    schedule 28.06.2016
comment
Хорошее решение, единственное, что я хотел бы упомянуть, это то, что ваша логика добавления привязки к словарю немного ошибочна. Когда вы делаете if var, вы фактически создаете копию массива в словаре, поэтому добавление к нему не повлияет на исходный словарь. Я бы предложил воспользоваться тем фактом, что необязательная цепочка возвращает Void? и сделать следующее: if events[name]?.append(binding) == nil {events[name] = [binding]} - person Hamish; 28.06.2016
comment
Спасибо, я этого не заметил. Мне нравится, как вы используете нулевую цепочку опций в качестве условия для инициализации нового массива. Думаю, я этого не заметил, потому что еще не добавил второго слушателя к тому же событию. Кроме того, я все еще привык к языкам, в которых массивы являются ссылочными типами. Я обновил приведенный выше код, включив в него ваше изменение и второй слушатель. - person OliverD; 29.06.2016
comment
В конечном счете проблема сводится к тому, что строго типизированный язык действительно не хочет, чтобы у вас был массив смешанных типов. Конечно, вы можете сжать их все в [AnyObject], но тогда вам придется постоянно приводить все подряд, если вы хотите иметь возможность что-то делать с данными. Таким образом, с учетом этого трения, возможно, единая модель диспетчера событий/центра сообщений не подходит, и вместо этого мне следует подумать о нескольких линиях связи, по одной для каждого типа передаваемых данных. - person OliverD; 29.06.2016
comment
...Возможно, мне следует встроить службу обмена сообщениями в сами компоненты. Что-то вроде делегата, за исключением того, что у вас будет массив из нескольких делегатов? (и это будет время выполнения и возможность горячей замены, а не время компиляции, как шаблон протокола делегата Swift) - person OliverD; 29.06.2016
comment
Другая реализация, которая очень похожа здесь: blog.scottlogic.com/2015/ 05.02/swift-events.html Интересно сравнить это с другими ссылками, которые были размещены выше, например, mikeash.com/pyblog/ и github.com/100mango/SwiftNotificationCenter - person OliverD; 29.06.2016

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

enum Signature{
    case cgvector(CGVector -> Void)
    case nstimeinterval(NSTimeInterval -> Void)

Недостатком является то, что перечисление фиксирует сильную ссылку на метод. Однако (мне нужно взять это из игровой площадки, чтобы проверить это больше), это, похоже, не создает сильный эталонный цикл. Вы можете установить содержащую сущность на nil, и все ее компоненты будут деинициализированы. Я не совсем уверен, что там происходит. Мне этот подход с перечислением кажется более чистым, чем помещение универсальной оболочки в массив AnyObject и постоянное приведение и стирание типа.

Комментарии и критика приветствуются.

/*:
 ## Entity - Component framework with a notification system for decoupled communications between components

 ### Limitations:
 1. Closure class stores a strong reference to the components. But, a strong reference cycle is not created. 
 2. A given class instance (component) can only subscribe to a given event with one method.
 */

import Cocoa
import Foundation

enum EventName: String {
    case onInput
}

//A type-safe wrapper that stores closures of varying signatures, and allows them to be identified by the hashValue of its owner.
class Closure {

    enum Signature {
        case cgvector(CGVector -> Void)
        case nstimeinterval(NSTimeInterval -> Void)

        func invoke(argument: Any){
            switch self {
            case let .cgvector(closure):        closure(argument as! CGVector)
            case let .nstimeinterval(closure):  closure(argument as! NSTimeInterval)
            }
        }
    }

    var method: Signature
    weak var owner: Component?

    init(owner: Component, action: Closure.Signature) {
        method = action
        self.owner = owner
    }

}

// Entity

class Entity {

    var components = Set<Component>()
    private var events = [EventName: [Closure]]()

    deinit {
        print("Entity deinit")
    }

    // MARK: component methods

    func add(component: Component){
        components.insert(component)
        component.parent = self
    }

    func remove(component: Component){
        unsubscribeFromAll(component)
        components.remove(component)
    }

    func remove<T: Component>(type: T.Type){
        guard let component = retrieve(type) else {return}
        remove(component)
    }

    func retrieve<T: Component>(type: T.Type) -> T? {
        for item in components {
            if item is T { return item as? T}
        }
        return nil
    }

    // MARK: event methods

    func subscribe(listener: Component, method: Closure.Signature, to event: EventName ){
        let closure = Closure(owner: listener, action: method)
        // if event array does not yet exist, create it with the closure.
        if events[event] == nil {
            events[event] = [closure]
            return
        }
        // check to make sure this listener has not subscribed to this event already
        if ((events[event]!.contains({ $0.owner! == listener })) == false) {
            events[event]!.append(closure)
        }
    }

    func dispatch(argument: Any, to event: EventName ) {
        events[event]?.forEach(){ $0.method.invoke(argument) }
    }

    func unsubscribe(listener: Component, from name: EventName){
        //events[name]? = events[name]!.filter(){ $0.hashValue != listener.hashValue }
        if let index = events[name]?.indexOf({ $0.owner! == listener }) {
            events[name]!.removeAtIndex(index)
        }
    }

    func unsubscribeFromAll(listener: Component){
        for (event, _) in events {
            unsubscribe(listener, from: event)
        }
    }

}


//Components

class Component: Hashable {
    weak var parent: Entity?
    var doc: String { return "Component" }
    var hashValue: Int { return unsafeAddressOf(self).hashValue }

    deinit {
        print("deinit \(doc)")
    }

}

func == <T: Component>(lhs: T, rhs: T) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

//: #### Usage test

class Input: Component {
    override var doc: String { return "Input" }


    func goRight() {
        parent?.dispatch(CGVector(dx: 10, dy: 0), to: .onInput )
    }

    func goUp() {
        parent?.dispatch(CGVector(dx: 0, dy: -10), to: .onInput )
    }
}

class Movement: Component {
    override var doc: String { return "Movement" }

    func move(vector: CGVector) {
        print("moved \(vector)")
    }
}


class Physics: Component {
    override var doc: String { return "Physics" }

    func update(time: NSTimeInterval){
        print("updated at \(time)")
    }

    func move(vector: CGVector) {
        print("updated \(vector)")
    }
}

// an example factory
var entity: Entity? = Entity()
if let instance = entity {

    // a couple of ways of adding components
    var inputComponent = Input()

    instance.add(inputComponent)
    instance.add(Movement())
    instance.add(Physics())

    var m = instance.retrieve(Movement.self)
    instance.subscribe(m!, method: .cgvector(m!.move), to: .onInput)

    let p = instance.retrieve(Physics.self)!
    instance.subscribe(p, method: .cgvector(p.move), to: .onInput)
    inputComponent.goRight()
    inputComponent.goUp()
    instance.retrieve(Input.self)?.goRight()

    instance.remove(Movement.self)
    m = nil

    inputComponent.goRight()
}
entity = nil //not a strong ref cycle
person OliverD    schedule 01.07.2016

Я попал в эту ситуацию, но я нашел классное решение с анонимной встроенной функцией, это похоже на отображение, вот пример


var cellConfigurator: ((UITableViewCell, _ index: IndexPath) -> Void)?

func setup<CellType: UITableViewCell>(cellConfig: ((CellType, _ index: IndexPath) -> ())?)
{
        // this mini function maps the closure
        cellConfigurator = { (cell: UITableViewCell, _ index: IndexPath) in
            if let cellConfig = cellConfig, let cell = cell as? CellType {
                cellConfig(cell, index)
            }
            else
            { print("-- error: couldn't cast cell") }
        }

}

person Mohamed Hashem    schedule 27.11.2019