Error: 'MyProtocol' is not convertible to 'AnyObject'

Hi forum,

I’m trying to store objects conforming to a protocol in a generic datastructure. (A weak set of delegates, but the details do not matter for this question).

I’m stuck here already:

struct MyStruct {}
class MyClass<T: AnyObject> {}
protocol MyProtocol: AnyObject {
	var nonObjc: MyStruct { get }
}
MyClass<MyProtocol>() // error: 'MyProtocol' is not convertible to 'AnyObject'

When I make MyProtocol @objc, it would work, but I cannot, as MyProtocol contains non-objc representable types.

Any ideas why this does not work? Limitiation of the compiler? Any workarounds?

3 Likes

I believe that’s because MyProtocol is not an AnyObject, but types conforming to it are. Can you give a more useful use-case to see if there’s a way around it?

Hm, but with protocol MyProtocol: AnyObject, the protocol inherits from AnyObject, so a MyProtocol really should be an AnyObject too.

Here’s a more concrete example:

private class Weak<T: AnyObject> {
	weak var value: T?
	init(value: T) {
		self.value = value
	}
}

class WeakArray<T: AnyObject> {
	private var elements: [Weak<T>] = []
	func addWeakly(_ element: T) {
		elements.append(Weak(value: element))
	}
	func nonDeallocatedElements() -> [T] {
		return elements.flatMap({ $0.value })
	}
}

struct InfoStruct {}
protocol AListener: AnyObject {
	func somethingDidChange(_ info: InfoStruct)
}

class A {
	private var listeners = WeakArray<AListener>()
	func addListener(_ listener: AListener) {
		listeners.addWeakly(listener)
	}
	func somethingChanged() {
		listeners.nonDeallocatedElements().forEach {
			$0.somethingDidChange(InfoStruct())
		}
	}
}

class B: AListener {
	init(a: A) {
		a.addListener(self)
	}
	func somethingDidChange(_ info: InfoStruct) {
		print("somethingDidChange")
	}
}

let a = A()
let b = B(a: a)

a.somethingChanged()

protocol existentials, as being discussed in here as well at the moment, do not conform to what concrete instances will.
What protocol MyProtocol: AnyObject says, with current Swift, is that any instance conforming to MyProtocol need to conform to AnyObject as well. This doesn’t say anything about protocol existentials, or in your case specifically protocol metatypes.

A workaround I can suggest here that should work, is to use a type-erased concrete type. i.e.

final class AnyListener: AListener {
    private weak var listener: AListener?
    init(_ listener: AListener) {
        self.listener = listener
    }
    func somethingDidChange(_ info: InfoStruct) {
        listener?.somethingDidChange(info)
    }
}

class A {
	private var listeners = WeakArray<AnyListener>()
	func addListener(_ listener: AListener) {
		listeners.addWeakly(AnyListener(listener))
	}
	...
}

Thanks for the explanation and workaround. I implemented a slightly different workaround using casting rather than type erasure, but I see no particular pros/cons between the two workarounds. Instead of having a property of type WeakArray<AListener>, I use a wrapping struct that stores the listeners as AnyObject internally, but provides a typesafe interface, and casts inbetween (casts will always succeed):

class A {
	private var listeners = WeakArrayWrapper()
	func addListener(_ listener: AListener) {
		listeners.addWeakly(listener)
	}
	func somethingChanged() {
		listeners.nonDeallocatedElements().forEach {
			$0.somethingDidChange(InfoStruct())
		}
	}
}

struct WeakArrayWrapper {
	private let listeners = WeakArray<AnyObject>()

	func notYetDeallocatedElements() -> [AListener] {
		return listeners.notYetDeallocatedElements().flatMap({ return $0 as? AListener })
	}

	func addListener(_ listener: AListener) {
		listeners.addWeakly(element: listener)
	}
}

I use a different workaround. In addition to a single generic Weak type (a struct in my case since), I have a protocol for Weak API and weak functionality is provided as its default implementation. I create non-generic weak holder types for each existential protocol. Using my helper types, it would be:

class A {

    struct WeakAListener: WeakReference {
        private(set) weak var ref: AListener?
        init(_ reference: AListener?) { self.ref = reference }
    }
    typealias AListeners = WeakSequence<WeakAListener>

    private var listeners = AListeners()

    func addListener(_ listener: AListener) {
        listeners.add(listener)
    }

    func somethingChanged() {
        for listener in listeners {
            listener.somethingDidChange(InfoStruct())
        }
    }
}

Ok, this looks very similar to the solution as the one from @DeFrenZ above, apart from using a generic helper type WeakSequence instead of wrapping the value manually.

The implementation reason for why this doesn’t work is because Swift protocol values have to keep track of two things: the value being referenced, and what we call the “witness table”, which describes how the dynamic type conforms to the protocol. (You can think of this like a struct of functions, one for each requirement in the protocol, though it’s a little more complicated than that in practice.) That means it can’t be treated as a single reference like a concrete class instance can.

Objective-C protocols have a different implementation strategy. Since Objective-C method calls are done entirely through dynamic dispatch based on selectors (method names), there’s no need to record how a particular class implements a protocol. Instead, you just assume that all the methods will be present dynamically. That’s convenient for things like this, but also doesn’t support some features of pure Swift protocols like associated types.

There is another option here: since all class instances make it easy to get back to the class, we could store a class-constrained protocol value as a single reference, and look up how the class conforms to the protocol every time we need it—pretty much what @fabb’s solution does manually. But that makes doing anything with the protocol value more expensive. Maybe some day in the future we’d add a special annotation that says “okay, yes, I want to make that trade-off so that I can use this as an AnyObject-compatible value”, but I don’t think we’d want to make it the default for all class-constrained protocol values.

Wow, thanks for the clear and easy to understand explanation!

I think a clever approach will provide acceptable levels of performance for class-constrained protocols, maybe something inspired by weak reference side tables. This issue comes up often enough and is annoying and confusing enough to warrant a solution. People have been asking for this for ages. Would a solution affect the ABI?

Yes, it would, because you’d have to be able to rely on the existence of such a table. I think it’s more like the Objective-C method cache than the weak reference side table—it’s a per-class hash lookup from protocol to conformance info (witness table). It’d still be slower than the approach we have today, though, and it’s not clear that that would be sufficient win over the existing top-level conformance cache to be worth the extra complexity in implementation.

In that case, the window for addressing this issue is closing fast. I wish we could do something about this. It also reminds me of the more general issue: SR-55. Although these issues are explainable by implementation details of the compiler, it doesn’t make it any less confusing and painful.

So, should I conclude that we are stuck with these practically forever? /cc @Slava_Pestov

Well, that’s not strictly true. We could add this as a new feature in the future, but you’d only be able to use it on Apple platforms that have the supported version of the Swift runtime. (And it would definitely have to be opt-in then, for obvious reasons.)