为什么 Self 和 self 有时在静态函数中指代不同的类型? [英] Why do Self and self sometimes refer to different types in static functions?

查看:14
本文介绍了为什么 Self 和 self 有时在静态函数中指代不同的类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

最近,我一直在使用 Swift 开发多个面向协议的应用程序框架,并注意到协议扩展中静态函数的一些(看似)奇怪的行为,特别是从元类型调用扩展函数的地方.

我最初发现这些行为的方式是解决一个错误,其中对象的类型以一种看似不可能的方式改变.我追踪了这个问题并最终确定这是因为在一个静态函数中,Selfself 可能拥有不同的类型(注意:我已经开始调用这些大 S 自我"和小自我").我将使用我在 Playground 中创建的一些简单示例来演示这一点:

class SomeBaseClass: SomeProtocol {}class SomeChildClass: SomeBaseClass {}协议 SomeProtocol {}扩展 SomeProtocol {静态私有 func getName() ->细绳 {返回 "(self): (type(of: self))"}static func ambiguousName() ->细绳 {返回 getName()}静态函数 littleName() ->细绳 {返回 self.getName()}静态函数 bigName() ->细绳 {返回 Self.getName()}}让孩子: SomeBaseClass.Type = SomeChildClass.self//SomeChildClass.Typeprint(child.ambiguousName())//"SomeChildClass: SomeBaseClass.Type
"print(child.littleName())//"SomeChildClass: SomeBaseClass.Type
"print(child.bigName())//"SomeBaseClass: SomeBaseClass.Type
"print(SomeChildClass.ambiguousName())//"SomeChildClass: SomeChildClass.Type
"print(SomeChildClass.littleName())//"SomeChildClass: SomeChildClass.Type
"print(SomeChildClass.bigName())//"SomeChildClass: SomeChildClass.Type
"print(SomeBaseClass.ambiguousName())//"SomeBaseClass: SomeBaseClass.Type
"print(SomeBaseClass.littleName())//"SomeBaseClass: SomeBaseClass.Type
"print(SomeBaseClass.bigName())//"SomeBaseClass: SomeBaseClass.Type
"

可以看出,当从元类型调用静态函数时,如果将该元类型分配给具有父类元类型声明类型的变量,结果可能会有所不同.

我的问题是 Self 如何知道它是什么类型?那么 self 如何知道它是什么类型?我不明白为什么 self 甚至可以在静态函数中访问,因为首先没有实例.我原以为应该只使用 Self,但现在我认为情况并非如此,因为 Selfself 已经证明在某些情况下产生不同的结果.

此外,当省略 Selfself 时,是否有任何理由使用 self 的类型,如在 return 语句中ambiguousName()函数中返回getName()?

对我来说,我认为最奇怪的部分是当 type(of: self)child.littleName()<调用时返回 SomeBaseClass.Type/code> 函数调用.动态类型"不应该仍然是 SomeChildClass 吗?

解决方案

TL;DR

Self 在协议扩展中的值由一组复杂的因素决定.在静态级别使用 self 或在实例级别使用 type(of: self) 代替 Self 几乎总是更可取的.这可确保您始终使用调用该方法的动态类型,从而防止出现奇怪的意外.

<小时>

首先让我们简化一下你的例子.

protocol P {在里面()}扩展P{静态函数 createWithBigSelf() ->自己 {返回自我()}静态函数 createWithLittleSelf() ->自己 {返回 self.init()}}A类:P{必需的 init() {}}B 类:A {}让 t: A.Type = B.self打印(t.createWithBigSelf())//A打印(t.createWithLittleSelf())//B

我们可以看到使用 Self 会返回一个新的 A 实例,而使用 self 会返回一个新的 实例B.

要理解为什么会这样,我们需要准确理解 Swift 是如何调用协议扩展方法的.

查看 IR,createWithBigSelf() 的签名是:

define hidden void @static (extension in main):main.P.createWithBigSelf() ->一个 (%swift.opaque* noalias nocapture sret,//指向应该存储返回值的位置的不透明指针%swift.type* %Self,//用作 Self 的元类型.i8** %Self.P,//元类型的协议见证表.%swift.type*//调用方法的实际元类型(self).) #0 {

(createWithLittleSelf() 的签名几乎相同.)

4个不可见参数由编译器生成——一个用于返回指针,一个用于符合类型的协议见证表,以及两个swift.type*参数来表示selfSelf.

这意味着可以传递不同元类型来表示selfSelf.

看看这个方法是怎么调用的:

//获取 B (B.self) 的元类型.%3 = 为 main.B() #4 调用 %swift.type* @type 元数据访问器//将其存储到类型为 A.Type 的 t.存储 %swift.type* %3, %swift.type** @main.t : main.A.Type,对齐 8//从 t 加载元类型.%4 = 加载 %swift.type*, %swift.type** @main.t : main.A.Type,对齐 8//获取 A 的元类型.%5 = 为 main.A() #4 调用 %swift.type* @type 元数据访问器//使用以下参数调用 P.createWithBigSelf()...调用 void @static (main 中的扩展):main.P.createWithBigSelf () -> A(%swift.opaque* noalias nocapture sret bitcast (//要存储的地址%C4main1A** @main.freshA : main.A to %swift.opaque*//返回值 (freshA)),%swift.type* %5,//A 的元类型——用于 Self.i8** getelementptr inbounds (//A 符合 P 的协议见证表.[1 x i8*],[1 x i8*]* @protocol 见证表 main.A : main.P in main, i32 0, i32 0),%swift.type* %4//存储在 t (B.self) 的元类型 – 用于 self.)

我们可以看到 A 的元类型被传递给 Self,而 B 的元类型(存储在 t) 被传递给 self.如果您认为 createWithBigSelf() 的返回类型在 A.Type 类型的值上调用将是 A.因此,SelfA.self,而 self 仍然是 B.self.

作为一般规则,Self 的类型由调用该方法的事物的静态类型决定.(因此,在您调用 bigName() 的情况下,Self.getName()SomeBaseClass.self 上调用 getName()).

这也适用于实例方法,例如:

//...扩展P{func createWithBigSelf() ->自己 {返回自我()}func createWithLittleSelf() ->自己 {返回类型(of: self).init()}}//...让 b: A = B()打印(b.createWithBigSelf())//A打印(b.createWithLittleSelf())//B

使用A.selfSelfB 实例的self 调用这些方法>.

<小时>

存在物

当您开始处理存在项时,事情变得复杂得多(参见 关于他们的伟大的 WWDC 谈话).如果您直接调用扩展方法(即它们不是协议要求),那么对于实例方法,Self 的值由在存在容器中将其装箱时的值,例如:

让 b: A = B()let p: P = b//b 的元类型存储为 A.self.打印(p.createWithBigSelf())//A()打印(p.createWithLittleSelf())//B()

让 b = B()let p: P = b//b 的元类型存储为 B.self.打印(p.createWithBigSelf())//B()打印(p.createWithLittleSelf())//B()

发生的事情是存在容器还存储值的元类型(以及值缓冲区和协议和值见证表),这是在装箱时从其静态类型中获取的.然后将该元类型用于 Self,导致上面演示的有些令人惊讶的行为.

使用元类型存在(例如 P.Type),存在容器只将元类型与协议见证表一起存储.然后,在调用 P 扩展中的静态方法时,该元类型用于 Selfself,当该方法不是协议要求时.

协议要求实现的方法将通过符合该协议的类型的协议见证表动态地分派到.在这种情况下,Self 的值被替换为直接符合协议的类型(虽然我不完全确定编译器为什么要这样做).>

例如:

protocol P {静态函数 testBigSelf()}扩展P{静态函数 testBigSelf() {打印(自我.自我)}}A 类:P {}B 类:A {}let t: P.Type = A.self//存在 P.Type 中的框t.testBigSelf()//A让 t1: P.Type = B.selft1.testBigSelf()//A

在这两种情况下,对 testBigSelf() 的调用都是通过 A 的协议见证表动态调度的,以符合 P (B 没有自己的协议见证表以保证P的一致性).因此Self就是A.self.这与实例方法完全相同.

这最常出现在通用函数中,这些函数通过协议见证表*动态分配协议要求.例如:

func foo(t: T) {t.testBigSelf()//通过 A 的 PWT 动态调度以符合 P.}foo(t: A())//Afoo(t: B())//A

AB 的实例被传入无关紧要 - testBigSelf() 是通过 A 分派的 的 PWT 符合 P,因此 SelfA.self.

(* 虽然编译器可以通过生成通用函数的专用版本来优化,但这并不会改变观察到的行为.)

<小时>

结论

在大多数情况下,Self 的类型由调用方法的任何静态类型决定.self 的值只是调用该方法的值 itself(静态方法的元类型,实例方法的实例),作为隐式传入参数.

我们发现的完整细分是selfSelf &协议扩展中的type(of: self)是:

  • 静态作用域(static 方法/计算属性)

    • self:调用方法的元类型值(因此必须是动态的).存在元类型没有任何区别.

    • Self:调用方法的元类型的 static 类型的元类型值(即当在给定的 T.输入,其中T:PSelfT.self).当在 existential 元类型 P.Type 上调用该方法并且不是协议要求时,Self 等效于 self(即是动态的).当方法协议要求时,Self等价于直接符合P.

    • type(of: self):元类型self的动态元类型.没那么有用.

  • 实例范围(非静态方法/计算属性)

    • self:调用方法的实例.这里没有惊喜.

    • Self:调用方法的实例的 static 类型的元类型值(即当在给定的 T 其中 T : P, SelfT.self).当在 existential P 上调用时,当方法不是协议要求时,这是实例的静态类型当它被装箱时.当方法协议要求时,Self等价于直接符合P.

    • type(of: self):调用方法的实例的动态元类型值.存在并没有什么不同.

由于决定 Self 值的因素非常复杂,在大多数情况下,我建议使用 selftype(of: self) 代替.这样被咬的几率就会小很多.

<小时>

回答您的其他问题

<块引用>

此外,当省略 Selfself 时,是否有任何理由使用 self 的类型,如在 return 语句中ambiguousName()函数中返回getName()?

事情就是这样——getName() 只是self.getName() 的语法糖.如果是 Self.getName() 的语法糖,它将与实例方法不一致,因为在实例方法中 Self 是元类型,而 self是实际的实例——访问其他实例成员更为常见,而不是来自给定实例方法的类型成员.

<块引用>

对我来说,我认为最奇怪的部分是当 type(of: self)child.littleName()<调用时返回 SomeBaseClass.Type/code> 函数调用.动态类型"不应该仍然是 SomeChildClass 吗?

是的,这也让我感到困惑.我希望 child 的动态类型是 SomeChildClass.Type 而不是 SomeBaseClass.Type.事实上,我什至会说它可能是一个错误(请随时在 bugs 上提交报告.swift.org 看看 Swift 团队是怎么做的).尽管在任何情况下,元类型的元类型都非常无用,因此它的实际价值是无关紧要的.

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
"
print(child.littleName())             // "SomeChildClass: SomeBaseClass.Type
"
print(child.bigName())                // "SomeBaseClass: SomeBaseClass.Type
"

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

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

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 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.

这篇关于为什么 Self 和 self 有时在静态函数中指代不同的类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆