Dependency Injection Fun with AnyObject

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?

So the answer seems to be...

func cache<T>(id: Int, instance: T) {
    if type(of: instance) is AnyClass {
        cache[id] = WeakBox(boxed: instance as AnyObject)
    }
}

Once we've determined that the object is a class it's now safe to perform the AnyObject cast. If it's not a class then it's simply not cached as there's no way to honor the 'shared' scope with a value type.

I may be overlooking something, but have you tried doing this?

func cache<T: AnyObject>(id: Int, instance: T) {
    cache[id] = WeakBox(boxed: instance)
}

Unfortunately, that just kicks the can down the road. That solves this problem, but now the function calling cache needs someway to determine if its generic T conforms to T:AnyObject.

We've just moved the problem to the calling function.

Then I would argue that there's a more fundamental design problem, in that you're trying to be too generic and enable storing (weak) references to non-reference types.

You could employ another layer of indirection and do something like this:

final class Box<T> {
    let value: T

    init(_ value: T) {
        self.value = value
    } 
}

func cache<T>(id: Int, instance: T) {
    cache[id] = WeakBox(boxed: Box<T>(instance))
}

where Box is the same thing as WeakBox but, well, not weak — however, this will not really achieve the goal, as clients will have to store Box<T> itself instead of copying the inner value T out for the box not to get dropped.

The solution that you provide above just circumvents the problem of the strategy not being workable for value types by simply silently failing at cacheing them. However, it will still stay a problem for the caller — the caller isn't aware if T is a class or not, thus happily passing the value to cache which takes the empty else path if T is not a class, resulting in doing absolutely nothing.

I guess that depends on how we define the semantics of the .shared scope. We can't have weak references to value types and as such they can't be shared.

So I can...

  1. Fail to cache the object, resulting in a new value instance being created by the factory.
  2. Cache the object and completely ignore the shared semantics. The value will never go out of scope.
  3. Throw a fatal error in debug, alerting the developer to the problem and then doing 1 or 2 as a fallback.

Resolver, my previous DI system, would do #1 on a value type and I'm inclined to continue that here.

So the above answer isn't the correct one. Just for reference, the final working and tested implementation is:

if let box = box(instance) {
    cache[id] = box
 }

fileprivate override func box<T>(_ instance: T) -> AnyBox? {
    if let optional = instance as? OptionalProtocol {
        if let unwrapped = optional.wrappedValue, type(of: unwrapped) is AnyObject.Type {
            return WeakBox(boxed: unwrapped as AnyObject)
        }
    } else if type(of: instance as Any) is AnyObject.Type {
        return WeakBox(boxed: instance as AnyObject)
    }
    return nil
}

Backed by

private protocol OptionalProtocol {
    var hasWrappedValue: Bool { get }
    var wrappedType: Any.Type { get }
    var wrappedValue: Any? { get }
}

extension Optional: OptionalProtocol {
    var hasWrappedValue: Bool {
        switch self {
        case .none:
            return false
        case .some:
            return true
        }
    }
    var wrappedType: Any.Type {
        Wrapped.self
    }
    var wrappedValue: Any? {
        switch self {
        case .none:
            return nil
        case .some(let value):
            return value
        }
    }
}

/// Internal box protocol for scope functionality
private protocol AnyBox {
    var instance: Any { get }
}

/// Strong box for cached and singleton scopes
private struct StrongBox<T>: AnyBox {
    let boxed: T
    var instance: Any {
        boxed as Any
    }
}

/// Weak box for shared scope
private struct WeakBox: AnyBox {
    weak var boxed: AnyObject?
    var instance: Any {
        boxed as Any
    }
}

There needs to be significant checking for optionals, and when it's not casting instance: T to any in the type(of: instance as Any) is essential for correctly handling protocols that may be wrapping class instances.

1 Like