11

Why doesn't Swift allow me to assign value Foo<U> to a variable of type Foo<T>, where U is a subclass of T?

For example:

class Cheese {
    let smell: Int
    let hardness: Int
    let name: String

    init(smell: Int, hardness: Int, name: String) {
        self.smell = smell
        self.hardness = hardness
        self.name = name
    }

    func cut() {
        print("Peeyoo!")
    }
}

class Gouda: Cheese {
    let aged: Bool

    init(smell: Int, hardness: Int, name: String, aged: Bool) {
        self.aged = aged
        super.init(smell: smell, hardness: hardness, name: name)
    }

    override func cut() {
        print("Smells delicious")
    }
}

class Platter<Food> {
    var food: Food

    init(food: Food) {
        self.food = food
    }
}

let goudaCheese = Gouda(smell: 6, hardness: 5, name: "Gouda", aged: false)
let goudaPlatter = Platter(food: goudaCheese)  //Platter<Gouda>

//error: cannot assign value of type 'Platter<Gouda>' to type 'Platter<Cheese>'
let platter: Platter<Cheese> = goudaPlatter

But why doesn't it work? You can assign to a variable an object that's a subclass of it's type, e.g.

let gouda = Gouda(smell: 6, hardness: 5, name: "Gouda", aged: false)
let cheese: Cheese = gouda

And you can add subclasses to collections:

let plainCheese = Cheese(smell: 2, hardness: 5, name: "American")
let gouda = Gouda(smell: 6, hardness: 5, name: "Gouda", aged: false)
var cheeses: [Cheese] = [plainCheese]
cheeses.append(gouda)

So how is let platter: Platter<Cheese> = goudaPlatter different? Are there any circumstances where it would be unsafe if it worked? Is it simply a limitation of the current version of Swift?

ConfusedByCode
  • 1,137
  • 8
  • 27
  • Not positive, but can you change `Platter` to `Platter` (and subsequently `var food: T`, `init(food: T)`)? – Connor Neville Oct 19 '16 at 19:03
  • 3
    Generics are invariant in Swift – in the eyes of the type-system, `Platter` and `Platter` are completely unrelated. If you want to convert between them, you need to write the code to perform that conversion. `Array` is just a special case where the compiler does some magic behind the scenes in order to perform this conversion for you (see for example [this Q&A](http://stackoverflow.com/questions/37188580/why-isnt-somestruct-convertible-to-any)). – Hamish Oct 19 '16 at 19:28
  • Related (maybe even dupe?): [How do I store a value of type Class in a Dictionary of type \[String:Class\] in Swift?](http://stackoverflow.com/q/38590548/2976878) – Hamish Oct 19 '16 at 20:00
  • It's similar, but I don't think it's the same. That question is about protocols, while this is about subclasses. Also, that question was specifically about collections. Rob Napier's answer, where he showed how the type system would break, was excellent, but I'm struggling to mentally apply it to my question. How would `let platter: Platter = goudaPlatter` break the type system? – ConfusedByCode Oct 19 '16 at 20:50
  • I think I'm starting to understand, but adding a `didSet` to food and trying to do access `food.aged` won't even compile. The class can only access methods and properties common to all types (and if I added constraints to the generic, members common to the constraints). So Platter can't contain any methods that try to access unique properties of Cheddar or any type. So I don't that situation can actually occur. – ConfusedByCode Oct 19 '16 at 21:05
  • @ConfusedByCode Ah sorry, my bad – that was a naff example. Let's suppose you have another subclass of `Cheese` – `Chedder`. Now let's assume that the compiler allows you to make the assignment `let platter: Platter = goudaPlatter`. Now it's legal to assign a `Chedder` instance to `platter.food`, even though the underlying instance types it as `Gouda`. What would happen if you printed `goudaPlatter.food.aged`? (there's no guarantee `Chedder` has an `aged` property) – Hamish Oct 19 '16 at 21:41
  • Ah, I understand. In other words, it's because `let platter: Platter = goudaPlatter` is a reference to the `goudaPlatter` instance. Therefore, reassigning `platter.food` reassigns `goudaPlatter.food`, and `goudaPlatter.food` obviously needs to be the correct subclass. Thanks. If you answer it I'll mark it correct. Maybe you can also include how it related to value types that conform to protocols. That would potentially be the best answer on the many variations of this question that I've seen. – ConfusedByCode Oct 20 '16 at 00:29
  • 1
    @ConfusedByCode Sorry, I *completely* forgot to get back to you on that! I have actually recently-ish answered [a similar Q&A here](http://stackoverflow.com/q/41976844/2976878). Please let me know if this now fully answers your question :) – Hamish Mar 28 '17 at 16:19

1 Answers1

7

You can work around this using a technique called type erasure. Basically you create a "wrapper" struct which hides the underlying class detail from the generic. It's not ideal, but it allows you to accomplish something similar to what you're trying to do.

class Cheese {
    func doSomethingCheesy() {
        print("I'm cheese")
    }
}

class Gouda: Cheese {
    override func doSomethingCheesy() {
        print("I'm gouda")
    }
}

struct AnyCheese {
    let cheese: Cheese
}

class Container<T> {
    init(object: T) {
        self.object = object
    }
    let object: T
}

let cheese = Cheese()
let cheeseContainer: Container<AnyCheese> = Container(object: AnyCheese(cheese: cheese))

let gouda = Gouda()
let goudaContainer: Container<AnyCheese> = Container(object: AnyCheese(cheese: gouda))

cheeseContainer.object.cheese.doSomethingCheesy() // prints "I'm cheese"
goudaContainer.object.cheese.doSomethingCheesy()  // prints "I'm gouda"
Chris Vig
  • 8,552
  • 2
  • 27
  • 35
  • 2
    I up-voted your answer, because it's a good explanation of type erasure and I'm sure it will help people who find this question. However, I didn't mark it as accepted because I was asking for a explanation of why Swift behaves that way, rather than looking for a work around. I think the comments on my question could be re-worked into a good answer. – ConfusedByCode Jan 07 '17 at 17:03