EDIT: The previous answers alluded to in the comments don’t answer the question, which was how to determine if any given Type was a reference type and how to safely conform said type to AnyObject.
Testing against the passed type doesn’t work, as the underlying type could be optional, or it could be a protocol, in which case one needs to know the passed instance is a class type or value type.
The solution I came up with is similar to the revised answer provided below.
So I have a new dependency injection framework, Factory.
Factory allows for scoped instances, basically allowing you to cache services once they’re created. And one of those scopes is shared. Any instance shared will be cached and returned just as long as someone in the outside world maintains a strong reference to it. After the last reference releases the object the cache releases the object and a new instance will be created on the next resolution.
This is implemented, obviously, as simply maintaining a weak reference to the created object. If the weak reference is nil it’s time to create a new object.
And therein lies the problem
Weak references can only apply to reference types.
Factory uses generics internally to manage type information. But I can create Factories of any type: Classes, structs, strings, whatever.)
Scopes use dictionaries of boxed types internally. If an instance exists in the cache and in the box it’s returned. So what I’d like to do is create this…
private struct WeakBox<T:AnyObject>: AnyBox {
weak var boxed: T
}
The AnyObject conformance is need in order to allow weak. You get a compiler error otherwise. Now I want to box and cache an object in my shared scope with something like this…
func cache<T>(id: Int, instance: T) {
cache[id] = WeakBox(boxed: instance)
}
But this also gives a compiler error. (Generic struct WeakBox requires T to be a class type.)
So how to bridge from on to the other? Doing the following doesn’t work. Swift shows a warning that "Conditional cast from ‘T’ to ‘AnyObject’ always succeeds" and then converts the type anyway.
func cache<T>(id: Int, instance: T) {
if let instance = instance as? AnyObject {
cache[id] = WeakBox(boxed: instance)
}
}
I’d be happy with the following, but again, same problem. You can’t test for class conformance and you can’t conditionally cast to AnyObject. Again, it always succeeds.
private struct WeakBox: AnyBox {
weak var boxed: AnyObject?
}
func cache<T>(id: Int, instance: T) {
if let instance = instance as? AnyObject {
cache[id] = WeakBox(boxed: instance)
}
}
What I’m doing at the moment is something like…
private struct WeakBox: AnyBox {
weak var boxed: AnyObject?
}
func cache<T>(id: Int, instance: T) {
cache[id] = WeakBox(boxed: instance as AnyObject)
}
Which works, but that instance as AnyObject
cast depends on some very weird Swift to Objective-C bridging behavior.
Not being able to test for class conformance at runtime is driving me bonkers, and seems like a semi-major loophole in the language.
You can’t test for conformance, and you can’t cast for conformance.
So what can you do?
2
Answers
So this took a while to figure out and even longer to track down the clues needed for a solution, so I'm providing my own code and answer to the problem
Given the following protocol...
And a box type to hold a weak reference...
Then the code to test and box a give type looks like...
Note that the type passed in could be a class, or a struct or some other value, or it could be a protocol. And it could be an optional version of any of those things.
As such, if it's optional we need to unwrap it and test the actual wrapped type to see if it's a class. If it is, then it's safe to perform our AnyObject cast.
If the passed value isn't optional, then we still need to check to see if it's a class.
There's also a
StrongBox
type used for non-shared type caching.And the final cache routine looks like this.
Source for the entire project is in the Factory repository.
As Martin notes in a comment, any value can be cast to
AnyObject
in Swift, because Swift will wrap value types in an opaque_SwiftValue
class, and the cast will always succeed. There is a way around this, though.The way to check whether a value is a reference type without this implicit casting is to check whether its type
is AnyObject.Type
, like so:Note that it’s crucial that you check whether the type
is AnyObject.Type
notis AnyObject
.T.self
, the object representing the type of the value, is itself an object, sois AnyObject
will always succeed. Instead,is AnyObject.Type
asks "does this inherit from the metatype of all objects", i.e., "does this object which represents a type inherit from an object that represents all object types?"Edit: Evidently, I’d forgotten that Swift includes
AnyClass
as a synonym forAnyObject.Type
, so the check can be simplified to beis AnyClass
. However, leaving the above as a marginally-expanded explanation for how this works.If you want this method to also be able to handle
Optional
values, you’re going to have to do a bit of special-casing to add support. Specifically, becauseOptional<T>
is anenum
regardless of the type ofT
, you’re going to need to reach in to figure out whatT
is.There are a few ways to do this, but because
Optional
is a generic type, and it’s not possible to ask "is this value anOptional<T>
?" without knowing whatT
is up-front, one of the easier and more robust ways to do this is to introduce a protocol whichOptional
adopts that erases the type of the underlying value while still giving you access to it:We can then use this protocol to our advantage in
cache
:This approach handles all of the previous cases, but also infinitely-deeply-nested
Optional
s, and protocol types inside ofOptional
s: