Why can a typed property not be assigned to a generic property?

In the example below, why can Swift not assign an explicitly type property of String or Double to a generic property of type T? However, if I use a generic constraint to specify an explicit type, then it does work?

import Foundation

class BadBox<T> {
  var value: T
  
  init(_ value: String) {
    self.value = value // Cannot assign value of type 'String' to type 'T'.
  }
  
  init(_ value: Double) {
    self.value = value // Cannot assign value of type 'String' to type 'T'.
  }
}

class GoodBox<T> {
  var value: T

  init(_ value: T) where T == String {
    self.value = value // This is OK and compiles fine.
  }

  init(_ value: T) where T == Double {
    self.value = value // As does this.
  }
}

It occasionally comes up that I want to define a class who's public interface explicitly defines the types of properties it can be used with, but who's internal implementation has generic components.

Creating an empty Protocol just to use as a generic constraint for such a class, ex: Boxable, seems needlessly verbose to me, though perhaps that's the Swift way?

In both BadBox<T> and GoodBox<T>, T is a generic parameter. This however doesn't mean that T can be any type. T is fixed for each instance.

The error message in the first initializer is self-explanatory: you cannot assign a value of type String to a value of type T. init(_ value: String), as defined, would be available independently of the generic parameter T and this means that the following would be allowed:

let string = "Swift"

let x = BadBox<String>(string)  // uses BadBox<String>.init(_ value: String)
let y = BadBox<Double>(string)  // uses BadBox<Double>.init(_ value: String)

In the first example T == String, so x.value is a String and self.value = value would be an allowed assignment.
In the second example, however, T == Double, so y.value is a Double, but you cannot assign a String value to a Double instance.

1 Like

Indeed, as Stefano says.

One thing to understand about generics is that the concrete types for their generic parameters are quite often chosen not by you, but by the user of your code. If I were to use your BadBox, I could imagine myself creating some, say, struct MyStruct and try creating a variable of type BadBox<MyStruct> — I am free to do so because I can insert into the angular brackets whatever I decide.

But obviously, BadBox's initializers can't work with that: for instance, init(_ value: String) would try to put a 24-byte value (which String is) into smaller storage if MyStruct happens to be, say, 8-byte-sized. Just declaring an initializer does not tell the compiler what you want T to be restricted to.

So you have to restrict the use of your generic type by using where clauses. GoodBox does this, so the type simply becomes not constructible with any other types that I personally would try to specify, so GoodBox<MyStruct> can't exist, only GoodBox<String> or GoodBox<Double>.

This is generally the mechanism you would use to restrict the types that T can be bound to, not only in initializers, but in other methods (for instance, collections restrict their element type to Comparable if they want to provide support for sorting).

And to be clear, creating an empty protocol is definitely not quite the Swift way to do it, as this is a semantic issue: if you use an empty protocol that any type can conform to, then why do you restrict initializers to certain types in the first place? Protocol restrictions are generally provided because you actually want some functionality guarantees, not disjunctions at the level of concrete types.

1 Like
Terms of Service

Privacy Policy

Cookie Policy