swift

Strange behavior in Swift meta type checking


I am playing around with the answer in this question: How to test whether generic variable is of type AnyObject

That solution doesn't seem to be working anymore. I have this code:

public struct ObjectHashable<T>: Hashable {
  
  public let object: T
  
  public init(object: T) {
    
    let t1 = type(of: object)
    let b1 = t1 is AnyClass
    print(t1) // Plugin
    print(b1) // false
    
    let t2 = T.self
    let b2 = t2 is AnyClass
    print(t2) // Plugin 
    print(b2) // false
    
    let t3 = Mirror(reflecting: object).subjectType 
    let b3 = t3 is AnyClass 
    print(t3) // P
    print(b3) // true 

    self.object = object
    
  }
  
  public static func ==(lhs: Self, rhs: Self) -> Bool {
    return true
  }
  
  public func hash(into hasher: inout Hasher) {
  }
}

protocol Plugin: AnyObject {}
class P: Plugin {}

typealias PluginHashable = ObjectHashable<Plugin>

PluginHashable(object: P())

This is a surprising behavior.

1.

let t1 = type(of: object)

type(of:) should return the dynamic type, which is P, not Plugin. But I got "Plugin" printed out.

2.

let b1 = t1 is AnyClass

Isn't it supposed to be true since t1 is reference type? (based on answer here: How to test whether generic variable is of type AnyObject)

3.

let t2 = T.self

This prints out Plugin, which makes sense, because I put Plugin in <>

4.

let b2 = t2 is AnyClass

This is false, but I think it's supposed to be true for the same reason

5.

    let t3 = Mirror(reflecting: object).subjectType // P

This prints out P, which makes sense

6.

    let b3 = t3 is AnyClass // true

This is true, which makes sense. But why is it different from the other 2 cases?


Solution

  • This unexpected behaviour is documented in the section named Finding the Dynamic Type in a Generic Context:

    Normally, you don’t need to be aware of the difference between concrete and existential metatypes, but calling type(of:) can yield unexpected results in a generic context with a type parameter bound to a protocol. In a case like this, where a generic parameter T is bound to a protocol P, the type parameter is not statically known to be a protocol type in the body of the generic function. As a result, type(of:) can only produce the concrete metatype P.Protocol.

    In your example, P would be the Plugin protocol. To expand on the explanation in the documentation, the type(of:) function has some unusual type-checking rules.

    Namely, if the compiler sees that its parameter is an existential type, it will allow the type of the whole type(of:) expression to be a concrete metatype, which is otherwise a totally unrelated type to the metatype of the existential type.

    In ObjectHashable however, the type of object is T. The compiler does not know that this will be bound to the existential type any Plugin later down the line, so it does not apply this special type checking rule for existential types. type(of: object) is basically equivalent to T.self.

    The behaviour of type(of:) has been changed to work like this in this commit, which happened after the linked answer has been posted.

    As the documentation suggests, casting object to an existential type (the easiest is Any) before putting it into type(of:) would work as expected.

    type(of: object as Any) is AnyClass // true
    

    Regarding 2 and 4, there really isn't not much to explain. They're just not classes. Just because the Plugin is class-constrained doesn't mean that the existential type any Plugin is a class. Existential types are very different from concrete types. SE-0335 proposed that all existential types should be prefixed with any to highlight this difference.

    [...]

    Despite these significant and often undesirable implications, existential types have a minimal spelling. Syntactically, the cost of using one is hidden, and the similar spelling to generic constraints has caused many programmers to confuse existential types with generics. In reality, the need for the dynamism they provided is relatively rare compared to the need for generics, but the language makes existential types too easy to reach for, especially by mistake. The cost of using existential types should not be hidden, and programmers should explicitly opt into these semantics.

    [...]

    I propose to make existential types syntactically explicit in the language using the any keyword. This proposal introduces the new syntax in the Swift 5 language mode, and this syntax should be required for existential types under a future language mode.