Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do Self and self sometimes refer to different types in static functions?

Recently I have been developing multiple heavily protocol-oriented application frameworks with Swift and noticed a few (seemingly) odd behaviors with static functions in protocol extensions, specifically where the extension functions are invoked from metatypes.

The way I initially discovered these behaviors was in troubleshooting a bug where the type of an object changed in a seemingly impossible way. I traced the problem down and eventually determined that it is because in a static function, Self and self can potentially hold different types (note: I've taken to calling these "Big S Self" and "Little s self" respectively). I'll demonstrate this with a bare bones example from something I whipped up in a Playground:

class SomeBaseClass: SomeProtocol {}

class SomeChildClass: SomeBaseClass {}

protocol SomeProtocol {}

extension SomeProtocol {
    static private func getName() -> String {
        return "\(self): \(type(of: self))"
    }

    static func ambiguousName() -> String {
        return getName()
    }

    static func littleName() -> String {
        return self.getName()
    }

    static func bigName() -> String {
        return Self.getName()
    }
}

let child: SomeBaseClass.Type = SomeChildClass.self // SomeChildClass.Type

print(child.ambiguousName())          // "SomeChildClass: SomeBaseClass.Type\n"
print(child.littleName())             // "SomeChildClass: SomeBaseClass.Type\n"
print(child.bigName())                // "SomeBaseClass: SomeBaseClass.Type\n"

print(SomeChildClass.ambiguousName()) // "SomeChildClass: SomeChildClass.Type\n"
print(SomeChildClass.littleName())    // "SomeChildClass: SomeChildClass.Type\n"
print(SomeChildClass.bigName())       // "SomeChildClass: SomeChildClass.Type\n"

print(SomeBaseClass.ambiguousName())  // "SomeBaseClass: SomeBaseClass.Type\n"
print(SomeBaseClass.littleName())     // "SomeBaseClass: SomeBaseClass.Type\n"
print(SomeBaseClass.bigName())        // "SomeBaseClass: SomeBaseClass.Type\n"

It can be seen that when static functions are invoked from a metatype, the result may differ if that metatype is assigned to a variable with a declared type of a parent class's metatype.

My question is how does Self know what type it is? How then does self know what type it is? It didn't make sense to me why self was even accessible in a static function anyway, since there is no instance in the first place. I would have thought that one should use Self exclusively, but now I'm thinking this isn't the case since Self and self have proven to produce different results in some scenarios.

Additionally, is there any reason why self's type is used when either Self or self is omitted, as in the return statement return getName() in the ambiguousName() function?

For me, I think the weirdest part is when type(of: self) returns SomeBaseClass.Type when called from the child.littleName() function invocation. Shouldn't the "dynamic type" still be of SomeChildClass?

like image 964
Michael Fourre Avatar asked Feb 04 '17 07:02

Michael Fourre


People also ask

What is difference between self and self in Swift?

This “Self” has an uppercase “S”, which is how you can tell it apart from the lowercase self. In Swift, Self refers to a type – usually the current type in the current context. Just as lowercase self can mean any current object, uppercase Self can mean just about any current type.

How are static functions different?

Unlike global functions in C, access to static functions is restricted to the file where they are declared. Therefore, when we want to restrict access to functions, we make them static. Another reason for making functions static can be reuse of the same function name in other files.

Can static methods be called with self?

You can call static methods on self , because self is an instance of the class that has the static method, and thus has the method. You can also call static methods on classes directly, because a static method doesn't require an object instance as a first argument - which is the point of the static method.

Can self be used to refer to static methods Swift?

The value of self inside the static methodThe metatype that you call the static method on is available to you in the method as self (it's simply passed as an implicit parameter). Therefore if you call doIt() on type(of: self) , self will be the dynamic metatype of the instance.


1 Answers

TL;DR

The value of Self in a protocol extension is determined by a complex set of factors. It's almost always preferable to use self at static level, or type(of: self) at instance level in place of Self. This ensures that you're always working with the dynamic type that the method is called on, preventing weird surprises.


First of all let's simplify your example down a bit.

protocol P {
    init()
}

extension P {
    static func createWithBigSelf() -> Self {
        return Self()
    }
    static func createWithLittleSelf() -> Self {
        return self.init()
    }
}

class A : P {
    required init() {}
}

class B : A {}


let t: A.Type = B.self

print(t.createWithBigSelf()) // A
print(t.createWithLittleSelf()) // B

We can see that using Self will return a new instance of A, whereas using self will return a new instance of B.

To understand just why this is the case, we need to understand exactly how Swift calls protocol extension methods.

Looking at the IR, the signature for createWithBigSelf() is:

define hidden void @static (extension in main):main.P.createWithBigSelf () -> A (
 %swift.opaque* noalias nocapture sret, // opaque pointer to where return should be stored

 %swift.type* %Self, // the metatype to be used as Self.

 i8** %Self.P, // protocol witness table for the metatype.

 %swift.type* // the actual metatype the method is called on (self).
 ) #0 {

(Signature for createWithLittleSelf() is almost identical.)

4 invisible arguments are generated by the compiler – one for a pointer for the return, one for the protocol witness table of the conforming type, and two swift.type* arguments to represent self and Self.

This therefore means that different metatypes can be passed to represent self or Self.

Looking at how this method is called:

  // get metatype for B (B.self).
  %3 = call %swift.type* @type metadata accessor for main.B() #4

  // store this to to t, which is of type A.Type.
  store %swift.type* %3, %swift.type** @main.t : main.A.Type, align 8

  // load the metatype from t.
  %4 = load %swift.type*, %swift.type** @main.t : main.A.Type, align 8

  // get A's metatype.
  %5 = call %swift.type* @type metadata accessor for main.A() #4

  // call P.createWithBigSelf() with the following parameters...
  call void @static (extension in main):main.P.createWithBigSelf () -> A(

    %swift.opaque* noalias nocapture sret bitcast (       // the address to store
      %C4main1A** @main.freshA : main.A to %swift.opaque* // the return value (freshA)
    ),

    %swift.type* %5, // The metatype for A – this is to be used for Self.

    i8** getelementptr inbounds ( // The protocol witness table for A conforming to P.
      [1 x i8*], 
      [1 x i8*]* @protocol witness table for main.A : main.P in main, i32 0, i32 0
    ),

    %swift.type* %4 // The metatype stored at t (B.self) – this is to be used for self.
  )

We can see that A's metatype is getting passed in for Self, and B's metatype (stored in t) is getting passed in for self. This actually makes quite a lot of sense if you consider that the return type of createWithBigSelf() if called on a value of type A.Type will be A. Thus Self is A.self, while self remains B.self.

As a general rule then, the type of Self is determined by the static type of the thing that the method is called on. (Therefore in your case when you call bigName(), Self.getName() is calling getName() on SomeBaseClass.self).

This also holds for instance methods, for example:

// ...

extension P {
    func createWithBigSelf() -> Self {
        return Self()
    }
    func createWithLittleSelf() -> Self {
        return type(of: self).init()
    }
}

// ...

let b: A = B()

print(b.createWithBigSelf()) // A
print(b.createWithLittleSelf()) // B

The methods are called with a Self of A.self, and a self that's an instance of B.


Existentials

Things get much more complicated when you start working with existentials (see this great WWDC talk on them). If you're calling the extension methods directly (i.e they aren't protocol requirements), then for instance methods, the value of Self is determined by the static type of the value when you box it in the existential container, for example:

let b: A = B()
let p: P = b // metatype of b stored as A.self.

print(p.createWithBigSelf()) // A()
print(p.createWithLittleSelf()) // B()

let b = B()
let p: P = b // metatype of b stored as B.self.

print(p.createWithBigSelf()) // B()
print(p.createWithLittleSelf()) // B()

What happens is that the existential container also stores the metatype of the value (along with the value buffer and protocol and value witness tables), which is taken from its static type at the time of boxing. This metatype is then used for Self, leading to the somewhat surprising behaviour demonstrated above.

With metatype existentials (e.g P.Type), the existential container just stores the metatype along with the protocol witness table. This metatype is then used for both Self and self in a call to a static method in a P extension, when that method isn't a protocol requirement.

Methods that are implementations of protocol requirements will be dispatched to dynamically via the protocol witness table for the type conforming to that protocol. In this case, the value of Self is replaced by the type that directly conforms to the protocol (although I'm not entirely sure why the compiler does this).

For example:

protocol P {
    static func testBigSelf()
}

extension P {
    static func testBigSelf() {
        print(Self.self)
    }
}

class A : P {}
class B : A {}

let t: P.Type = A.self // box in existential P.Type
t.testBigSelf() // A

let t1: P.Type = B.self
t1.testBigSelf() // A

In both cases, the call to testBigSelf() is dispatched dynamically via A's protocol witness table for conformance to P (B doesn't get its own protocol witness table for P conformance). Therefore Self is A.self. It's exactly the same story with instance methods.

This most commonly comes up in generic functions, which dispatch protocol requirements dynamically via the protocol witness table*. For example:

func foo<T : P>(t: T) {
    t.testBigSelf() // dispatch dynamically via A's PWT for conformance to P.
}

foo(t: A()) // A
foo(t: B()) // A

It doesn't matter whether an instance of A or B is passed in – testBigSelf() is dispatched via A's PWT for conformance to P, therefore Self is A.self.

(* Although the compiler can optimise by generating specialised versions of generic functions, this doesn't change the observed behaviour.)


Conclusion

For the most part, the type of Self is determined by the static type of whatever the method is called on. The value of self is simply the value itself that the method is called on (a metatype for a static method, an instance for an instance method), passed in as an implicit parameter.

The full breakdown of what we discovered is that the values of self, Self & type(of: self) in protocol extensions are:

  • Static scope (static methods / computed properties)

    • self: The metatype value which the method is called on (therefore must be dynamic). Existential metatypes don't make a difference.

    • Self: The metatype value for the static type of the metatype that the method is called on (i.e when called on a given T.Type where T : P, Self is T.self). When the method is called on an existential metatype P.Type, and isn't a protocol requirement, Self is equivalent to self (i.e is dynamic). When the method is a protocol requirement, Self is equivalent to the metatype value of the type that directly conforms to P.

    • type(of: self): The dynamic metatype of the metatype self. Not that useful.

  • Instance scope (non-static methods / computed properties)

    • self: The instance that the method is called on. No surprises here.

    • Self: The metatype value for the static type of the instance that the method is called on (i.e when called on a given T where T : P, Self is T.self). When called on an existential P, when the method isn't a protocol requirement, this is the static type of the instance when it was boxed. When the method is a protocol requirement, Self is equivalent to the metatype value of the type that directly conforms to P.

    • type(of: self): The dynamic metatype value for the instance that the method is called on. Existentials don't make a difference.

Due to the sheer complexity of factors that determine what the value of Self is, in most cases I would recommend using self and type(of: self) instead. That way there's far less chance of being bitten.


Answering your additional questions

Additionally, is there any reason why self's type is used when either Self or self is omitted, as in the return statement return getName() in the ambiguousName() function?

That's just the way it is – getName() is merely syntactic sugar for self.getName(). It would be inconsistent with instance methods if were syntactic sugar for Self.getName(), as in instance methods Self is a metatype, whereas self is the actual instance – and it's much more common to be accessing other instance members, rather than type members from a given instance method.

For me, I think the weirdest part is when type(of: self) returns SomeBaseClass.Type when called from the child.littleName() function invocation. Shouldn't the "dynamic type" still be of SomeChildClass?

Yeah, that puzzles me too. I would expect the dynamic type of child to be SomeChildClass.Type rather than SomeBaseClass.Type. In fact, I'd even go so far as it say it might be a bug (feel free to file a report at bugs.swift.org to see what the Swift team make of it). Although in any case, the metatype of a metatype is pretty useless, so it's actual value is fairly inconsequential.

like image 123
Hamish Avatar answered Oct 08 '22 04:10

Hamish