6

I want to store a more specialized type in a Dictionary of type [String:SomeClass]. Here is some sample code illustrating my problem (also available to play with at https://swiftlang.ng.bluemix.net/#/repl/579756cf9966ba6275fc794a):

class Thing<T> {}

protocol Flavor {}

class Vanilla: Flavor {}

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

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

It produces the error ERROR at line 9, col 28: cannot assign value of type 'Thing<Vanilla>' to type 'Thing<Any>?'.

I've tried casting Thing<Vanilla>() as Thing<Flavor> but that produces the error cannot convert value of type 'Thing<Vanilla>' to type 'Thing<Flavor>' in coercion.

I've also tried to define the Dictionary as type [String:Thing<Any>] but that doesn't change anything either.

How do I create a collection of different Things without resorting to plain [String:AnyObject]?

I should also mention that the class Thing is not defined by me (in fact it's about BoltsSwift Tasks), so the solution to create a base class of Thing without a type parameter doesn't work.

Mike
  • 223
  • 3
  • 10
  • Generics are invariant in Swift – and for good reason too. Say you have a `class Chocolate : Flavour`, then a `Thing`. Now let's say you could put both a `Thing` & `Thing` in a `[Thing]`. If your `Thing` class has a property of type `T` – your array now says that you can assign either `Chocolate()` or `Vanilla()` to that property on either of your `Thing` or `Thing` instances, which is bonkers. What's the actual problem that you're trying to solve? – Hamish Jul 26 '16 at 13:15
  • I want to make a collection of BoltsSwift `Task`s (or `TaskCompletionSource`es) to return to callers which request the same workload while a request to process it is already running. The `Task`s are of different types depending on the type of the model returned by the particular workload. So I need _some_ way to express a type that just says "any `Thing`". – Mike Jul 26 '16 at 13:26
  • I settled on `protocol BaseThing {}`, `extension Thing: BaseThing` and then using `[String:BaseThing]` for the collection. Not exactly pretty but it works. – Mike Jul 26 '16 at 13:34
  • That probably matches best what you're describing specifically. It sounds like you're trying to hold a heterogeneous bag of "things that have specific types and you want to keep track of that specific type" and that generally is going to mean some vague type that later is `as` casted. Resolving this more cleanly often requires rethinking the system broadly to allow it from the start, and can't be patched with a local type. – Rob Napier Jul 26 '16 at 13:37

2 Answers2

11

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.)

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you, great answer! I have seen a lot of those type erasers in the Swift standard library and have wondered what they are used for. – return true Jul 26 '16 at 13:47
  • Note that your counterexample with `inout` only illustrates that there must be invariance for variables passed as `inout` arguments (as `inout` can both read and write to that variable); it's not because arbitrary generics are invariant (in fact, as you highlight in your edit, `Array` is a special case – `[Int]` is indeed a subtype of `[Any]`, but your example is still rightly illegal). You can also see this with no generics at all, for example `func foo(a: inout Any, b: Any) { a = b }; var i = 1; foo(a: &i, b: "Stuff")` is just as illegal. – Hamish Jul 31 '17 at 09:32
1

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())
Matan Lachmish
  • 1,235
  • 12
  • 17