Подтвердить что ты не робот

Почему "Я" и "я" иногда ссылаются на разные типы в статических функциях?

Недавно я разрабатывал несколько сильно ориентированных на протокол приложений фреймворков с Swift и заметил несколько (по-видимому) нечетное поведение со статическими функциями в расширениях протокола, особенно там, где функции расширения вызываются из метатипов.

То, как я изначально обнаружил это поведение, заключалось в устранении ошибки, в которой тип объекта был изменен, по-видимому, невозможным способом. Я проследил проблему и в конце концов определил, что это связано с тем, что в статической функции Self и Self могут потенциально содержать разные типы (примечание: я взял называть эти "Big S Self" и "Little s self", соответственно). Я продемонстрирую это с помощью примера с голыми костями из того, что я взбивал на игровой площадке:

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"

Можно видеть, что когда статические функции вызывается из метатипа, результат может отличаться, если этот метатип присваивается переменной с объявленным типом метатипа родительского класса.

Мой вопрос в том, как Self знать, какой тип он есть? Как тогда Self знает, какой тип он есть? Для меня не имело смысла, почему Self был даже доступен в статической функции в любом случае, поскольку в действительности нет экземпляра. Я бы подумал, что нужно использовать Self исключительно, но теперь я думаю, что это не так, поскольку Self и Self доказали, что в некоторых сценариях возникают разные результаты.

Кроме того, существует ли какая-либо причина, по которой тип Self используется, когда опускаются либо Self или Self, как в операторе return return getName() в функции ambiguousName()?

Для меня, я думаю, самая странная часть - это когда type(of: self) возвращает SomeBaseClass.Type при вызове из вызова функции child.littleName(). Не должен ли "динамический тип" быть SomeChildClass?

4b9b3361

Ответ 1

TL; DR

Значение Self в расширении протокола определяется сложным набором факторов. Почти всегда предпочтительно использовать self на статическом уровне или type(of: self) на уровне экземпляра вместо Self. Это гарантирует, что вы всегда работаете с динамическим типом, из которого вызывается метод, предотвращая странные сюрпризы.


Прежде всего позвольте немного упростить ваш пример.

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

Мы можем видеть, что использование Self вернет новый экземпляр A, тогда как использование self вернет новый экземпляр B

Чтобы понять, почему это так, нам нужно точно понять, как Swift вызывает методы расширения протокола.

Глядя на ИК, подпись для createWithBigSelf() выглядит так:

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 {

(Подпись для createWithLittleSelf() практически идентична.)

Компилятор генерирует 4 невидимых аргумента - один для указателя на возвращаемый объект, один для таблицы свидетелей протокола соответствующего типа и два аргумента swift.type* для представления self и Self.

Следовательно, это означает, что различные метатипы могут быть переданы для представления self или Self.

Посмотрим, как этот метод называется:

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

Мы можем видеть, что метатип A передается для Self, а метатип B (хранится в t) передается для self. Это на самом деле имеет большой смысл, если учесть, что возвращаемый тип createWithBigSelf() при вызове значения типа A.Type будет A Таким образом, " Self есть " A.self, а " self остается " B.self

Как правило, тип Self определяется статическим типом объекта, для которого вызывается метод. (Следовательно, в вашем случае, когда вы вызываете bigName(), Self.getName() вызывает getName() для SomeBaseClass.self).

Это также относится, например, к методам, например:

// ...

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

Методы вызываются с помощью Self of A.self и self которые являются экземпляром B


экзистенциалам

Все становится намного сложнее, когда вы начинаете работать с экзистенциалами (см. Этот великолепный доклад WWDC о них). Если вы вызываете методы расширения напрямую (т.е. они не являются требованиями к протоколу), то, например, для методов значение Self определяется статическим типом значения, когда вы помещаете его в экзистенциальный контейнер, например:

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()

Что происходит, так это то, что экзистенциальный контейнер также хранит метатип значения (вместе с буфером значений и таблицами протокола и значения), который берется из его статического типа во время упаковки. Этот метатип затем используется для Self, что приводит к несколько неожиданному поведению, продемонстрированному выше.

С существующими P.Type (например, P.Type) экзистенциальный контейнер просто хранит метатип вместе с таблицей-свидетелем протокола. Этот метатип затем используется как для Self и self в вызове статического метода в P расширения, когда этот метод не является обязательным требованием протокола.

Методы, которые являются реализациями требований протокола, будут динамически отправляться через таблицу-свидетель протокола для типа, соответствующего этому протоколу. В этом случае значение Self заменяется типом, который напрямую соответствует протоколу (хотя я не совсем уверен, почему компилятор делает это).

Например:

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

В обеих случаях, вызов testBigSelf() отправляются динамически через A таблицы свидетелей протокола на соответствие P (B не получает свою собственную таблицу следящего протокола для P соответствия). Поэтому " Self - это " A.self. Это точно такая же история с методами экземпляра.

Это чаще всего встречается в универсальных функциях, которые динамически распределяют требования к протоколу через таблицу свидетелей протокола *. Например:

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

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

Не имеет значения, является ли экземпляр A или B передается в - testBigSelf() отправляется с помощью A PWT на соответствие P, поэтому Self это A.self.

(* Хотя компилятор может оптимизировать, генерируя специализированные версии универсальных функций, это не меняет наблюдаемого поведения.)


Заключение

По большей части тип Self определяется статическим типом того, к чему вызывается метод. Значение self - это просто само значение, к которому вызывается метод (метатип для статического метода, экземпляр для метода экземпляра), передаваемое как неявный параметр.

Полное расстройство того, что мы обнаружили, заключается в том, что значения self, Self & type(of: self) в расширениях протокола:

  • Статическая область (static методы/вычисляемые свойства)

    • self: значение метатипа, для которого вызывается метод (поэтому оно должно быть динамическим). Существующие метатипы не имеют значения.

    • Self: значение метатипа для статического типа метатипа, к которому вызывается метод (т. T.Type При вызове для данного типа T.Type где T: P, Self - это T.self). Когда метод вызывается для экзистенциального P.Type и не является требованием протокола, Self эквивалентен self (т.е. является динамическим). Когда метод является требованием протокола, Self эквивалентен значению метатипа типа, который непосредственно соответствует P

    • type(of: self): динамический метатип метатипа self. Не так полезно.

  • Область действия экземпляра (non- static методы/вычисляемые свойства)

    • self: экземпляр, на котором вызывается метод. Здесь нет сюрпризов.

    • Self: значение метатипа для статического типа экземпляра, к которому вызывается метод (т. T.self При вызове для заданного T где T: P, Self - это T.self). При вызове экзистенциального P, когда метод не является требованием протокола, это статический тип экземпляра, когда он был упакован. Когда метод является требованием протокола, Self эквивалентен значению метатипа типа, который непосредственно соответствует P

    • type(of: self): значение динамического метатипа для экземпляра, для которого вызывается метод. Существование не имеет значения.

Из-за явной сложности факторов, определяющих значение Self, в большинстве случаев я бы рекомендовал вместо этого использовать self и type(of: self). Таким образом, гораздо меньше шансов быть укушенным.


Отвечая на ваши дополнительные вопросы

Кроме того, есть ли причина, по которой self тип используется, когда либо Self либо self опущены, как в операторе return getName() в функции ambiguousName()?

Так оно и есть - getName() - просто синтаксический сахар для self.getName(). Это было бы несовместимо с методами экземпляра, если бы они были синтаксическим сахаром для Self.getName(), так как в методах экземпляра Self является метатипом, тогда как self является фактическим экземпляром - и гораздо более распространенным является доступ к другим членам экземпляра, а не к членам типа. из данного метода экземпляра.

Для меня, я думаю, самая странная часть - это когда type(of: self) возвращает SomeBaseClass.Type при вызове из child.littleName() функции child.littleName(). SomeChildClass "динамический тип" не должен быть SomeChildClass?

Да, это меня тоже озадачивает. Я ожидаю, что динамический тип child будет SomeChildClass.Type а не SomeBaseClass.Type. На самом деле, я бы даже сказал, что это может быть ошибка (не стесняйтесь подать отчет на bugs.swift.org, чтобы узнать, что команда Swift сделает из этого). Хотя в любом случае метатип метатипа довольно бесполезен, поэтому его фактическое значение довольно несущественно.