问题
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
?
回答1:
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 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 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 eitherSelf
orself
is omitted, as in the return statementreturn getName()
in theambiguousName()
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)
returnsSomeBaseClass.Type
when called from thechild.littleName()
function invocation. Shouldn't the "dynamic type" still be ofSomeChildClass
?
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.
来源:https://stackoverflow.com/questions/42037852/why-do-self-and-self-sometimes-refer-to-different-types-in-static-functions