4

Consider the following in Swift:

struct GenericStruct<T> {}
class A {}
class B: A {}

func doSomething() -> GenericStruct<A> {
   return GenericStruct<B>()
}

This gives the error:

Cannot convert return expression of type GenericStruct<B> to return type GenericStruct<A>

But B is a subclass of A.

  • Why can't Swift convert GenericStruct<B> to GenericStruct<A>?
  • What should be done in this scenario?
funkybro
  • 8,432
  • 6
  • 39
  • 52

2 Answers2

14

Swift generics are invariant, meaning that two generic types are unrelated even if their generic type parameters have an inheritance relation. Invariant generic types cannot be cast to each other since they have no inheritance relation to each other.

This means that GenericStruct<B> and GenericStruct<A> are completely unrelated even if B is a subclass of A, hence you cannot cast one to the other.

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
8

Here's a counter example as to what could happen if this were allowed:

struct GenericStruct<T> {
    let value: T
}

class A {}
class B: A {}
class C: A {}

let wrapperOfB: GenericStruct<B> = GenericStruct(value: B())
let wrapperOfA: GenericStruct<A> = wrapperOfB // Suppose GenericStruct<B> was a subtype of GenericStruct<A>
wrapperOfA.value = C() // C is an A, so this should be legal

useB(wrapperOfB.value) // Now what?
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Is this at all a simple code example of what @David_Pasztor described in his answer? If so, why not include it in your answer? And please, if not, what does this code show? –  Jun 14 '18 at 16:27
  • 2
    My answer is supplemental to his. He's exactly correct, Swift's generics are invariant. In my answer, I detail what invalid code would be possible if OP's proposition were allowed. – Alexander Jun 14 '18 at 20:54
  • I was hesitant about uprooting your answer - I already upvoted the other one. With your comment, I'll upvote your's too. –  Jun 14 '18 at 20:57
  • Very helpful illustration thanks – funkybro Jun 20 '18 at 09:25
  • Considering semantics of structs in Swift, useB will always use reference stored in wrapperOfB, not it's copy in wrapperOfA, also the line where we assign C() would create a new value of GenericStruct. Variance is actually a working and useful concept, which can be seen even in some standard Swift types. – Konstantin Oznobihin Jul 01 '19 at 18:49
  • @KonstantinOznobihin Hmmm it seems I totally glossed over the struct-edness of this example. If `GenericStruct` were a class, my example would stand. So could covariance be possible for structs, since they can't be aliased? `which can be seen even in some standard Swift types` Only in a limited and hard-coded fashion, e.g. for things like returning `Array` where `Array` is expected. – Alexander Jul 06 '19 at 01:06
  • @Alexander and if GenericStruct was a class then your example will just show how one can misuse variance, but since there are already enough features in Swift which can be misused it still wouldn't explain why variance is or should be prohibited. Probably, it just wan't important enough feature and it still might be added in later versions of the language. – Konstantin Oznobihin Jul 08 '19 at 17:09
  • @KonstantinOznobihin It's not just misuse, it's invalid code. For variances to be supported in Swift, it's not just a matter of allowing code like to this compile. That would be undefined behaviour. It would need a formal language model, that adds restrictions on how covariant and contravarient types can be used, so that cases like this are handled successfully. See Scala's implementation, for example. – Alexander Jul 08 '19 at 17:21
  • @KonstantinOznobihin I've never said variances shouldn't be implemented, or that they're bad. It's that the current language model doesn't support them, and even if they did, they would lead to undefined behaviour unless new features were added. – Alexander Jul 08 '19 at 17:22
  • @Alexander Your code doesn’t show anything about current language model, and you’ll get undefined behaviour only for naive implementation. The proper one should be able to detect such issues in compile time. Look, for example, at C# generics, they don’t allow code with such issues. – Konstantin Oznobihin Jul 08 '19 at 18:09
  • @KonstantinOznobihin "and you’ll get undefined behaviour only for naive implementation. The proper one should be able to detect such issues in compile time" that's my point. – Alexander Jul 08 '19 at 18:27
  • @KonstantinOznobihin And your example (C#, more specifically its support for contravarience via the "out" specifier for generic type declarations) is similar to my example of Scala – Alexander Jul 08 '19 at 18:33
  • @KonstantinOznobihin Here's more details on what I was talking about (it's a pretty quick read): https://docs.scala-lang.org/tour/variances.html – Alexander Jul 09 '19 at 04:11