简体   繁体   中英

Swift polymorphic closure dispatch with struct/protocols

I have a case where I want to register either one argument or no argument closures with a service. There's always an argument available, but for brevity, I want to be able to register no arg closures as well, and then just dispatch the closure without the available argument in that case. Coming from a strong OO and dynamic types background where we love polymorphic dispatch and class inheritance trees and let the types figure themselves out, I can throw the following together:

class AbstractAction<T> {
    func publish(value:T) {
        fatalError("you should override this")
    }
}

class NullaryAction<T>: AbstractAction<T> {
    var closure:() -> ()
    override func publish(_:T) {
        closure()
    }
    init(closure:()->()) {
        self.closure = closure
    }
}

class UnaryAction<T>: AbstractAction<T> {
    var closure:(T) -> ()
    override func publish(value:T) {
        closure(value)
    }
    init(closure:(T)->()) {
        self.closure = closure
    }
}

var action:AbstractAction = UnaryAction<Int>(closure: { print("\($0)") })
action.publish(42)
action = NullaryAction<Int>(closure: { print("something happened") } )
action.publish(42)

So I see 42 followed by something happened in my console. Great.

But I'd like to explore doing this with struct and/or enum . Value semantics are all the rage. The enum approach was relatively straightforward, I think:

enum Action<T> {
    case Nullary( ()->() )
    case Unary( (T)->() )

    func publish(value:T) {
        switch self {
        case .Nullary(let closure):
            closure()
        case .Unary(let closure):
            closure(value)
        }
    }
}

var action = Action.Unary({ (arg:Int) -> () in print("\(arg)") })
action.publish(42)
action = Action<Int>.Unary( { print("shorthand too \($0)") } )
action.publish(42)
action = Action<Int>.Nullary({ print("something happened") })
action.publish(42)

To do a struct approach, I it is my understanding that I should use a protocol to capture common interface of publish(value:T) . But that's where things get confusing, because protocols apparently can't be mixed with generics? I tried:

struct NullaryAction<T> {
    typealias ValueType = T
    var closure:() -> ()
}

struct UnaryAction<T> {
    typealias ValueType = T
    var closure:(T) -> ()
}

protocol Action {
    typealias ValueType
    func publish(value:ValueType)
}

extension NullaryAction: Action {
    func publish(_:ValueType) {
        self.closure()
    }
}

extension UnaryAction: Action {
    func publish(value:ValueType) {
        self.closure(value)
    }
}

var action:Action = UnaryAction(closure: { (arg:Int) -> () in print("\(arg)") })
action.publish(42)
action = UnaryAction<Int>(closure: { print("shorthand too \($0)") } )
action.publish(42)
action = NullaryAction<Int>(closure:{ print("something happened") })
action.publish(42)

This just produces a lot of errors at the bottom. I had tried to do the extensions as generics (eg extension NullaryAction<T>:Action ), but it told me that T was unused, even though I had placed the typealias expressions in the extensions.

Is it possible to do this with struct/protocol? I'm happy with the enum solution, but was disappointed I couldn't realize it with the struct/protocol approach.

Judging by the fact that you want to cast your structs to their protocols (by using var action: Action = UnaryAction {...} ), I'm assuming you don't need the publish method to have the right signature upon evoking it.

In other words, by declaring your Action protocol with a typealias , the compiler is expecting to specialise the publish method for each instance of your structs.

This means that you have two options:

  1. Remove the type casting:

Example:

var action /*removed the :Action type casting */ = UnaryAction<Int>(closure: { (arg:Int) -> () in print("\(arg)") })
action.publish(42)
action = UnaryAction<Int>(closure: { print("shorthand too \($0)") } )
action.publish(42)
var anotherAction = NullaryAction<Int>(closure:{ print("something happened") }) //need to use another variable for this last one
anotherAction.publish(42)

This solution makes your publish methods also have the same signature that your structs have. If your struct is specialised to work with Ints , then you'll have .publish(value: Int) .

  1. Make the protocol non-generic

Example:

protocol Action {
    func publish(value:Any)
}

struct NullaryAction<T>: Action {
    let closure: () -> ()
    init(closure: () -> ()) {
        self.closure = closure
    }
    func publish(value:Any) {
        self.closure()
    }
}

struct UnaryAction<T>: Action {
    let closure: (T) -> ()
    init(closure: (T) -> ()) {
        self.closure = closure
    }
    func publish(value:Any) {
        self.closure(value as! T) //Need to type cast here
    }
}

var action: Action = UnaryAction<Int>(closure: { (arg:Int) -> () in print("\(arg)") })
action.publish(42)
action = UnaryAction<Int>(closure: { print("shorthand too \($0)") } )
action.publish(42)
action = NullaryAction<Int>(closure:{ print("something happened") })
action.publish(42)

This solution allows you to keep on type casting, but your publish methods will all have the same signature ( .publish(value: Any) ). You also need to account for that upon executing the closure.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM