mpangburn
(Michael Pangburn)
1
I find myself having trouble making code generic when using class-constrained protocols in contexts requiring AnyObject. Since a protocol used as a type does not conform to the protocols from which it inherits, the code snippets below do not compile.
A couple quick examples:
If we want to box an arbitrary value to hold a reference weakly, the boxed type must conform to AnyObject, but this prevents us from boxing instances class-constrained protocols:
struct WeakBox<T: AnyObject> {
weak var value: T?
}
protocol ClassConstrained: AnyObject { }
let x: WeakBox<ClassConstrained> // nope!
We encounter a similar issue with a simple wrapper around a protocol-based observation subscription model:
Details
protocol Observable {
associatedtype Observer: AnyObject
var observers: [ObjectIdentifier: Observer] { get set }
}
extension Observable {
mutating func addObserver(_ observer: Observer) {
let id = ObjectIdentifier(observer)
observers[id] = observer // (we'd toss some weak-boxing in here too)
}
}
protocol MyClassObserver: AnyObject {
func myClassDidSomething(_ instance: MyClass)
}
class MyClass: Observable {
typealias Observer = MyClassObserver // nope!
var observers: [ObjectIdentifier: MyClassObserver]
}
Is there a straightforward way to be able to use a class-constrained protocol as a type in contexts where a class is expected? We know any instance conforming to such a protocol will be a class, so it doesn't seem unreasonable—but I can't come with up anything nice that both works in a generic context and retains type safety.
It's possible to hide dynamic casting by limiting the API surface, but this still doesn't seem ideal:
struct WeakBox<T> {
private weak var _value: AnyObject?
var value: T? {
return _value as? T
}
init(_ value: T) {
self._value = value as AnyObject
}
}
Any suggestions?
1 Like
somu
(somu)
2
AnyObject is a protocol to which all classes implicitly conform.
Not sure if this is what you were looking for, if MyObserver is your class you could simply have it as
class MyObserver {} //It would implicitly conform to AnyObject
class ObservedClass: Observable {
typealias Observer = MyObserver
var observers = [ObjectIdentifier: Observer]()
}
mpangburn
(Michael Pangburn)
3
Sorry, perhaps I didn't provide enough detail. The idea is that MyObserver is some protocol which functions something like a delegate would, but the observed object holds a dictionary of observers to be able to notify more than just one. Something like this:
protocol MyClassObserver: AnyObject {
func myClassDidSomething(_ instance: MyClass)
}
class MyClass: Observable {
typealias Observer = MyClassObserver
var observers: [ObjectIdentifier: MyClassObserver]
}
I've updated the OP with this example for clarity.
somu
(somu)
4
I could be wrong, but I think you don't need an associated type for this scenario.
protocol MyClassObserver: AnyObject {
func myClassDidSomething(_ instance: MyClass)
}
protocol Observable {
var observers: [ObjectIdentifier: AnyObject] { get set }
}
extension Observable {
mutating func addObserver(_ observer: AnyObject) {
let id = ObjectIdentifier(observer)
observers[id] = observer // (we'd toss some weak-boxing in here too)
}
}
class MyClass : Observable {
var observers: [ObjectIdentifier : AnyObject] = [ObjectIdentifier : MyClassObserver]()
}
mpangburn
(Michael Pangburn)
5
This works, but at the cost of some type safety. If I want to call observer.myClassDidSomething for each observer in MyClass, I must dynamically cast the values in the dictionary to MyClassObserver instances.
somu
(somu)
6
Then you could just make that as a requirement in Observable.
The change is Observable protocol requires observers to be of the type [ObjectIdentifier: MyClassObserver]
protocol MyClassObserver: AnyObject {
func myClassDidSomething(_ instance: MyClass)
}
protocol Observable {
var observers: [ObjectIdentifier: MyClassObserver] { get set }
}
extension Observable {
mutating func addObserver(_ observer: MyClassObserver) {
let id = ObjectIdentifier(observer)
observers[id] = observer // (we'd toss some weak-boxing in here too)
}
}
class MyClass : Observable {
var observers = [ObjectIdentifier : MyClassObserver]()
}
mpangburn
(Michael Pangburn)
7
Then we lose flexibility since the model is no longer generic. The approach doesn't scale; Observable wouldn't work for MyOtherClass with MyOtherClassObserver.
Your two suggestions summarize the challenge here: how do we retain both type safety and extensibility?
mpangburn
(Michael Pangburn)
8
To take the focus off the observation API, here's another example of the underlying challenge.
If we want to box an arbitrary value to hold a reference weakly, the boxed type must conform to AnyObject, but this prevents us from boxing instances class-constrained protocols.
struct WeakBox<T: AnyObject> {
weak var value: T?
}
protocol ClassConstrained: AnyObject { }
let x: WeakBox<ClassConstrained> // nope!
3 Likes
somu
(somu)
9
This is a fair point, not sure why the compiler couldn't infer anything conforming to ClassConstrained would also conform to AnyObject
This is because class constrained protocols are not yet fully implemented.
1 Like
somu
(somu)
11
Thanks a lot @Nobody1707 for the clarification was a bit confused
DekVester
(someone)
12
The problem is, Swift doesn't allow you to create an instance of a generic with a protocol instead of a concrete type, if this generic's type-parameter is also constrained with a protocol. For example, you can not do this:
protocol Base {
}
protocol Derived: Base {
}
struct MyStruct<T: Base> {
}
var myStruct = MyStruct<Derived>()//ERROR: Using 'Derived' as a concrete type conforming to protocol 'Base' is not supported
This is because it opens up the doors for you to declare a static func on Base and then try to call it from inside of MyStruct type:
protocol Base {
static func baseFunc()
}
struct MyStruct<T: Base> {
func myFunc() {
T.baseFunc()
}
}
Now, the code above is okay when you just use it with a concrete type:
struct Concrete: Base {
}
var myStruct = MyStruct<Concrete>
myStruct.myFunc()
But when you use it with a protocol it is not okay:
var myStruct = MyStruct<Derived>
myStruct.myFunc()//not okay
, because Swift doesn't know how to call function .baseFunc on protocol Derived - because it does not have any particular Metatype to do so.
PS The workaround for this I'm aware of is to declare your protocols as @objc. It will unblock you to run this, but only unless you use static members in the protocols - this won't work in any case
I'm not sure what you mean. They certainly are implemented. Protocols do not conform to other protocols though, but that is a separate issue.
Nobody1707
(Nobody1707)
14
Sorry, I don't know what I was thinking there.
Even though any keyword was introduced lately, it's still impossible to create a collection of WeakBox<SomeProtocol> where SomeProtocol: AnyObject.