Почему использование dynamicType при развертывании силы, отличной от нулевого значения, используется?

Я действительно смутился, обнаружив, что следующий код просто отказывается сбой с типичным исключением "Неожиданно найденное ноль при распаковке необязательного значения", которое вы ожидаете от разворачивания силы bar.

struct Foo {
    var bar: Bar?
}
struct Bar {}

var foo = Foo()

debugPrint(foo.bar) // nil
debugPrint(foo.bar!.dynamicType) // _dynamicType.Bar

Кажется, что dynamicType как-то может отступить к определенному типу bar - без сбоев.

Обратите внимание, что это происходит только тогда, когда bar определяется как тип значения (поскольку @dfri говорит), Foo является struct или final class (как отмеченный @MartinR) и Foo изменен.

Вначале я считал, что это может быть просто оптимизация компилятора из-за того, что тип bar известен во время компиляции, поэтому разворот силы мог быть оптимизирован - но он также падает, когда bar определяется как final class. Кроме того, если я устанавливаю "Уровень оптимизации" на -Onone, он все равно работает.

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

Является ли это ошибкой или функцией с dynamicType, или я просто что-то пропустил здесь?

(Использование Xcode 7.3 w/Swift 2.2)


Обновление Swift 4.0.3

Это все еще воспроизводимо (с еще более минимальным примером) в Swift 4.0.3:

var foo: String?
print(type(of: foo!)) // String

Здесь мы используем преемник dynamicType, type(of:), чтобы получить динамический тип; и, как и в предыдущем примере, он не сбой.

Ответ 1

Это действительно ошибка, которая была исправлена ​​by этот запрос на растяжение, который должен сделать его выпуском Swift 4.2, все будет хорошо.


Если кто-то заинтересован в кажущихся странными требованиями воспроизвести его, то есть (не совсем) краткий обзор того, что происходит...

Вызовы стандартной функции библиотеки type(of:) разрешаются как особый случай с помощью проверки типа; Они заменяются в АСТ специальным "выражением динамического типа". Я не исследовал, как обрабатывался предшественник dynamicType, но я подозреваю, что он сделал что-то подобное.

При испускании промежуточного представления (SIL для конкретного) для такого выражения компилятор проверяет, будет ли вид, если получившийся метатип "толстый" (для экземпляров класса и протокола), и если это так испускает подвыражение (т.е. переданный аргумент) и получает его динамический тип.

Однако, если полученный метатип "тонкий" (для структур и перечислений), компилятор знает значение метатипа во время компиляции. Поэтому подвыражение нужно оценивать только в том случае, если оно имеет побочные эффекты. Такое выражение испускается как "проигнорированное выражение".

Проблема заключалась в том, что логика в испускании игнорируемых выражений также была lvalues ​​(выражение, которое может быть присвоено и передано как inout).

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

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

Таким образом, игнорируемое выражение lvalues ​​с компонентами разворота силы неверно не оценит развертывание силы, если они созданы только из физических компонентов.

Давайте посмотрим на пару случаев, которые в настоящее время вылетают (в Swift 4.0.3), и объясняют, почему ошибка была вставлена ​​в сторону и разворот силы был правильно оценен:

let foo: String? = nil
print(type(of: foo!)) // crash!

Здесь foo не является lvalue (как объявлено let), поэтому мы просто получаем его значение и силу разворачиваем.

class C {} // also crashes if 'C' is 'final', the metatype is still "thick"
var foo: C? = nil
let x = type(of: foo!) // crash!

Здесь foo является lvalue, но компилятор видит, что полученный метатип "толстый", и поэтому зависит от значения foo!, поэтому загружается lvalue и поэтому вычисляется разгрузка силы.

Давайте также рассмотрим этот интересный случай:

class Foo {
  var bar: Bar?
}
struct Bar {}

var foo = Foo()
print(type(of: foo.bar!)) // crash!

Он сработает, но это не будет, если foo отмечен как final. Получающийся метатип "тонкий" в любом случае, поэтому какая разница foo составляет final make?

Ну, когда foo не является окончательным, компилятор не может просто ссылаться на свойство bar по адресу, поскольку он может быть переопределен подклассом, который вполне может повторно реализовать его как вычисленное свойство. Таким образом, lvalue будет содержать логический компонент (вызов bar s getter), поэтому компилятор выполнит нагрузку, чтобы оценить потенциальные побочные эффекты этого геттер-вызова (и разворот силы будет также оцениваться в нагрузки).

Однако, когда foo является final, доступ к объекту bar может быть смоделирован как физический компонент, то есть его можно называть адресом. Поэтому компилятор неправильно предположил, что, поскольку все компоненты lvalue являются физическими, он может пропустить оценку.

В любом случае, эта проблема исправлена. Надеюсь, кто-то найдет вышеперечисленное полезное и/или интересное:)

Ответ 2

Поскольку dynamicType работает с типами, а не с значениями. Во время выполнения...

foo.bar.dynamicType == Swift.Optional<Bar>

Итак, когда вы разворачиваете опцию, вы получаете Bar