I would like to understand this section.
I'm reading Finding the Dynamic Type in a Generic Context that has this snippet:
func printGenericInfo<T>(_ value: T) {
let t = type(of: value)
print("'\(value)' of type '\(t)'")
}
protocol P {}
extension String: P {}
let stringAsP: P = "Hello!"
printGenericInfo(stringAsP)
// 'Hello!' of type 'P'
... that's followed up by this sentence:
This unexpected result occurs because the call to
type(of: value)
insideprintGenericInfo(_:)
must return a metatype that is an instance ofT.Type
, butString.self
(the expected dynamic type) is not an instance ofP.Type
(the concrete metatype of value).
String.self
not an instance of P.Type
when I can run this code?func f(_ t: P.Type) { print("...") }
f(String.self)
type(of:)
return the concrete metatype outside but not inside generic functions?print("'\(stringAsP)' of type '\(type(of: stringAsP))'")
// 'Hello!' of type 'String'
For a protocol P
, there are two kinds of metatypes that you can write with P
. You can write (any P).Type
(aka P.Protocol
), which is the metatype of P
itself, and any P.Type
(aka P.Type
), which is an existential type representing "the metatype of some type that conforms to P
".
String.self
is an instance of any P.Type
, but not (any P).Type
. The documentation is incorrect here. It probably meant to say (any P).Type
. Otherwise this whole thing doesn't make sense.
type(of:)
has two different behaviours depending on what kind of type its type parameter T
is. Your printGenericInfo
also has a type parameter T
, so to avoid confusion, I will call them typeof.T
and printGenericInfo.T
respectively.
If typeof.T
is a non-existential type, then it returns the metatype of typeof.T
. If typeof.T
is an existential (i.e. protocol) type any E
, it would return an any E.Type
, not (any E).Type
. After all, the former is much more useful - it can tell you the actual type that the existential type "wraps". type(of:)
in this case needs to "unwrap" the existential and "look inside" of it.
let s1 = ""
let s2: Any = s1
// typeof.T is String
type(of: s1)
// typeof.T is Any, an existential type, so type(of:) unwraps it and returns String.self
// this would be pretty useless if it just returned Any.self
type(of: s2)
This difference in behaviour is exactly what your printGenericInfo
lacks. In fact, this kind of semantics cannot be written in Swift's syntax. This is why type(of:)
has the weird signature it has - it returns a Metatype
type parameter, seemingly unrelated to the type it takes in.
type(of:)
uses special annotations to allow the compiler to type-check type(of:)
calls in a special way. Depending on typeof.T
, Metatype
could either be typeof.T.Type
(when typeof.T
is not existential) or any typeof.T.Type
(when typeof.T
is existential). Note that this is a compile time check. The type parameters of generic functions are decided at compile time, not at runtime.
printGenericInfo
doesn't unwrap printGenericInfo.T
and look inside of it. It just directly passes that to type(of:)
, so typeof.T
is decided to be printGenericInfo.T
, a type parameter, which is not an existential type.
When you call printGenericInfo(stringAsP)
, printGenericInfo.T
is decided to be any P
- an existential type. This doesn't change typeof.T
, which is still printGenericInfo.T
, a non-existential type. So at runtime, type(of:)
returns the metatype of any P
, and that is (any P).Type
.
If you do let t = type(of: value as Any)
instead, then type(of:)
will see that typeof.T
is an existential type (Any
), and so it will unwrap the existential type and look inside of it.