How do I store a value of type Class<ClassImplementingProtocol> in a Dictionary of type [String:Class<Protocol>] in Swift?

Solution 1:

A Thing<Vanilla> is not a Thing<Flavor>. Thing is not covariant. There is no way in Swift to express that Thing is covariant. There are good reasons for this. If what you were asking for were allowed without careful rules around it, I would be allowed to write the following code:

func addElement(array: inout [Any], object: Any) {
    array.append(object)
}

var intArray: [Int] = [1]
addElement(array: &intArray, object: "Stuff")

Int is a subtype of Any, so if [Int] were a subtype of [Any], I could use this function to append strings to an int array. That breaks the type system. Don't do that.

Depending on your exact situation, there are two solutions. If it is a value type, then repackage it:

let thing = Thing<Vanilla>(value: Vanilla())
dict["foo"] = Thing(value: thing.value)

If it is a reference type, box it with a type eraser. For example:

// struct unless you have to make this a class to fit into the system, 
// but then it may be a bit more complicated
struct AnyThing {
    let _value: () -> Flavor
    var value: Flavor { return _value() }
    init<T: Flavor>(thing: Thing<T>) {
        _value = { return thing.value }
    }
}

var dict = [String:AnyThing]()
dict["foo"] = AnyThing(thing: Thing<Vanilla>(value: Vanilla()))

The specifics of the type eraser may be different depending on your underlying type.


BTW: The diagnostics around this have gotten pretty good. If you try to call my addElement above in Xcode 9, you get this:

Cannot pass immutable value as inout argument: implicit conversion from '[Int]' to '[Any]' requires a temporary

What this is telling you is that Swift is willing to pass [Int] where you ask for [Any] as a special-case for Arrays (though this special treatment isn't extended to other generic types). But it will only allow it by making a temporary (immutable) copy of the array. (This is another example where it can be hard to reason about Swift performance. In situations that look like "casting" in other languages, Swift might make a copy. Or it might not. It's hard to be certain.)

Solution 2:

One way to solve this is adding an initialiser to Thing and creating a Thing<Flavor> that will hold a Vanilla object.

It will look something like:

class Thing<T> {

    init(thing : T) {
    }

}

protocol Flavor {}

class Vanilla: Flavor {}

var dict = [String:Thing<Flavor>]()

dict["foo"] = Thing<Flavor>(thing: Vanilla())