Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using protocol as a concrete type conforming to 'AnyObject' is not supported

I'm trying to implement a simple multi-delegate situation:

protocol Subscribable: class {
    associatedtype Subscriber: AnyObject
    var subscribers: NSHashTable<Subscriber> { get }
}

protocol ControllerSubscriber: class {
    func controllerDidSomething()
}

class Controller: Subscribable {
    typealias Subscriber = ControllerSubscriber
    var subscribers = NSHashTable<Subscriber>.weakObjects()  // Error
}

Error: Using 'ControllerSubscriber' as a concrete type conforming to protocol 'AnyObject' is not supported.

My question is:

  • What does this error mean exactly?
  • What are the underlying concepts the thing I'm trying to do fails for?
  • Why is this "not supported"?

And of course, how do I work around this? In the sense of an actual solution not a work around.

I have such a hard time understanding Swift's generics system. I seem to be running into seemingly simple situations like this constantly. I just want to put a thing conforming to a protocol into another thing :( . I would like to know where my thinking goes wrong so I can fix it and never have to see these errors again.

There is this related question but please note the answers give only workarounds, no explanations or solutions.

like image 949
tombardey Avatar asked Sep 29 '16 20:09

tombardey


1 Answers

It is probably not fair to blame this problem on Swift. Reasoning about types seems to be some meta-art we will first have to get used to (unless you have been sitting on the C++ standards committee for the last 30 years that is :-).

Turns out your problem is related to your choice of NSHashTable as the data structure to hold your subscribers. The following would compile with minimal changes:

protocol Subscribable: class {
    associatedtype Subscriber
    var subscribers: [Subscriber?] { get }
}

protocol ControllerSubscriber: class {
    func controllerDidSomething()
}

class Controller: Subscribable {
    typealias Subscriber = ControllerSubscriber
    var subscribers = [Subscriber?]()
}

however, it lacks the weak semantics and is not really useful yet. The list of subscribers is exhibited as a property and has to be manipulated directly by the clients. Furthermore each implementation of Subscribable has to implement its own notification mechanism and there is hardly any logic that is centralised by this approach. Technically you can use it like this:

class Controller: Subscribable {
    typealias Subscriber = ControllerSubscriber
    var subscribers = [Subscriber?]()

    func notify() {
        for case let subscriber? in subscribers {
            subscriber.controllerDidSomething()
        }
    }
}

var controller = Controller()

class IWillSubscribe : ControllerSubscriber {
    func controllerDidSomething() {
        print("I got something")
    }
}

controller.subscribers.append(IWillSubscribe())
controller.notify()

but that is neither very practical nor very readable. This would have been an acceptable solution (since it was the only one) up to Java 7, but even in Java 8 (and much more so in Swift) we would like to encapsulate the notification logic into the Subscribable protocol as a default implementation, but that would be another post.

Since you chose to implement subscribers as an NSHashTable (there is probably an ARC reason to desire weak references here) there seems to be some Objective-C trickery involved. After much experimentation (and finally finding the fourth answer to this question I got the following to work:

protocol Subscribable: class {
    associatedtype Subscriber : AnyObject
    var subscribers: NSHashTable<Subscriber> { get }
}

@objc protocol ControllerSubscriber: class {
    func controllerDidSomething()
}

class Controller: Subscribable {
    typealias Subscriber = ControllerSubscriber
    var subscribers = NSHashTable<Subscriber>.weakObjects()

    func notify() {
        for subscriber in subscribers.allObjects {
            subscriber.controllerDidSomething()
        }
    }
}

var controller = Controller()

class IWillSubscribe : ControllerSubscriber {
    func controllerDidSomething() {
        print("I got something")
    }
}

let iDoSubscribe = IWillSubscribe()
controller.subscribers.add(iDoSubscribe)
controller.notify()

which is virtually identical to your original (with some proof around it). As it seems Objective-C @protocols are not quite the same as Swift protocols, but Swift can actually do either.

There is quite a lot of subtlety in this though, only allObjects works without type erasure, your trusty objectEnumerator only returns Any? and that is a stupid animal to get anything from. Also note that

let iDoSubscribe = IWillSubscribe()

is instrumental. At first I tried

controller.subscribers.add(IWillSubscribe())

which actually added something to the count of subscribers, but went away with any attempt to iterate (as one should expect from a weak reference that is not referred to anywhere else).

A very late answer that is already way too long, just to prove that this is still an issue, even with Swift 3. Maybe this will get better once this Jira ticket is resolved.

like image 146
Patru Avatar answered Oct 01 '22 21:10

Patru