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
?
TL;DR
The value of
Self
in a protocol extension is determined by a complex set of factors. It's almost always preferable to useself
at static level, ortype(of: self)
at instance level in place ofSelf
. 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.
We can see that using
Self
will return a new instance ofA
, whereas usingself
will return a new instance ofB
.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:(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 representself
andSelf
.This therefore means that different metatypes can be passed to represent
self
orSelf
.Looking at how this method is called:
We can see that
A
's metatype is getting passed in forSelf
, andB
's metatype (stored int
) is getting passed in forself
. This actually makes quite a lot of sense if you consider that the return type ofcreateWithBigSelf()
if called on a value of typeA.Type
will beA
. ThusSelf
isA.self
, whileself
remainsB.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 callbigName()
,Self.getName()
is callinggetName()
onSomeBaseClass.self
).This also holds for instance methods, for example:
The methods are called with a
Self
ofA.self
, and aself
that's an instance ofB
.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: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 bothSelf
andself
in a call to a static method in aP
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:
In both cases, the call to
testBigSelf()
is dispatched dynamically viaA
's protocol witness table for conformance toP
(B
doesn't get its own protocol witness table forP
conformance). ThereforeSelf
isA.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:
It doesn't matter whether an instance of
A
orB
is passed in –testBigSelf()
is dispatched viaA
's PWT for conformance toP
, thereforeSelf
isA.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 ofself
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 givenT.Type
whereT : P
,Self
isT.self
). When the method is called on an existential metatypeP.Type
, and isn't a protocol requirement,Self
is equivalent toself
(i.e is dynamic). When the method is a protocol requirement,Self
is equivalent to the metatype value of the type that directly conforms toP
.type(of: self)
: The dynamic metatype of the metatypeself
. 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 givenT
whereT : P
,Self
isT.self
). When called on an existentialP
, 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 toP
.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 usingself
andtype(of: self)
instead. That way there's far less chance of being bitten.Answering your additional questions
That's just the way it is –
getName()
is merely syntactic sugar forself.getName()
. It would be inconsistent with instance methods if were syntactic sugar forSelf.getName()
, as in instance methodsSelf
is a metatype, whereasself
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.Yeah, that puzzles me too. I would expect the dynamic type of
child
to beSomeChildClass.Type
rather thanSomeBaseClass.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.