Свойства UnsafeMutablePointer.pointee и didSet
Я получил неожиданное поведение при использовании UnsafeMutablePointer для наблюдаемого свойства в созданной мной структуре (в Xcode 10.1, Swift 4.2). Смотрите следующий код детской площадки:
struct NormalThing {
var anInt = 0
}
struct IntObservingThing {
var anInt: Int = 0 {
didSet {
print("I was just set to \(anInt)")
}
}
}
var normalThing = NormalThing(anInt: 0)
var ptr = UnsafeMutablePointer(&normalThing.anInt)
ptr.pointee = 20
print(normalThing.anInt) // "20\n"
var intObservingThing = IntObservingThing(anInt: 0)
var otherPtr = UnsafeMutablePointer(&intObservingThing.anInt)
// "I was just set to 0."
otherPtr.pointee = 20
print(intObservingThing.anInt) // "0\n"
Похоже, что изменение указателя на UnsafeMutablePointer на наблюдаемое свойство фактически не изменяет значение свойства. Кроме того, при назначении указателя на свойство запускается действие didSet. Что мне здесь не хватает?
2 ответа
Каждый раз, когда вы видите такую конструкцию UnsafeMutablePointer(&intObservingThing.anInt)
Вы должны быть крайне осторожны в отношении того, будет ли оно проявлять неопределенное поведение. В подавляющем большинстве случаев так и будет.
Во-первых, давайте разберемся, что именно здесь происходит. UnsafeMutablePointer
не имеет инициализаторов, которые принимают inout
параметры, так какой инициализатор это вызов? Ну, компилятор имеет специальное преобразование, которое позволяет &
префиксный аргумент для преобразования в изменяемый указатель на "хранилище", на которое ссылается выражение. Это называется преобразованием вход-в-указатель.
Например:
func foo(_ ptr: UnsafeMutablePointer<Int>) {
ptr.pointee += 1
}
var i = 0
foo(&i)
print(i) // 1
Компилятор вставляет преобразование, которое превращает &i
в изменчивый указатель на i
Хранилище. Хорошо, но что происходит, когда i
нет хранения? Например, что если он вычислен?
func foo(_ ptr: UnsafeMutablePointer<Int>) {
ptr.pointee += 1
}
var i: Int {
get { return 0 }
set { print("newValue = \(newValue)") }
}
foo(&i)
// prints: newValue = 1
Это все еще работает, так на какое хранилище указывает указатель? Чтобы решить эту проблему, компилятор:
- Вызовы
i
и получает результирующее значение во временную переменную. - Получает указатель на эту временную переменную и передает его вызову
foo
, - Вызовы
i
Сеттер с новым значением из временного.
Эффективно делать следующее:
var j = i // calling `i`'s getter
foo(&j)
i = j // calling `i`'s setter
Надеемся, из этого примера должно быть ясно, что это накладывает важное ограничение на время жизни указателя, переданного в foo
- он может быть использован только для изменения значения i
во время звонка foo
, Попытка экранировать указатель и использовать его после вызова foo
приведет к изменению только значения временной переменной, а не i
,
Например:
func foo(_ ptr: UnsafeMutablePointer<Int>) -> UnsafeMutablePointer<Int> {
return ptr
}
var i: Int {
get { return 0 }
set { print("newValue = \(newValue)") }
}
let ptr = foo(&i)
// prints: newValue = 0
ptr.pointee += 1
ptr.pointee += 1
происходит после i
'setter был вызван с новым значением временной переменной, поэтому он не имеет никакого эффекта.
Хуже того, он демонстрирует неопределенное поведение, так как компилятор не гарантирует, что временная переменная останется действительной после вызова foo
закончился Например, оптимизатор может деинициализировать его сразу после вызова.
Хорошо, но пока мы получаем указатели только на переменные, которые не вычисляются, мы должны иметь возможность использовать указатель вне вызова, которому он был передан, верно? К сожалению, нет, оказывается, есть много других способов выстрелить себе в ногу, избегая преобразований между указателями!
Чтобы назвать только несколько (есть еще много!):
Локальная переменная проблематична по той же причине, что и наша временная переменная из предыдущих: компилятор не гарантирует, что она останется инициализированной до конца области, в которой она объявлена. Оптимизатор может де-инициализировать ее раньше.
Например:
func bar() { var i = 0 let ptr = foo(&i) // Optimiser could de-initialise `i` here. // ... making this undefined behaviour! ptr.pointee += 1 }
Сохраненная переменная с наблюдателями проблематична, потому что внутри она фактически реализована как вычисляемая переменная, которая вызывает своих наблюдателей в своем установщике.
Например:
var i: Int = 0 { willSet(newValue) { print("willSet to \(newValue), oldValue was \(i)") } didSet(oldValue) { print("didSet to \(i), oldValue was \(oldValue)") } }
является по существу синтаксическим сахаром для:
var _i: Int = 0 func willSetI(newValue: Int) { print("willSet to \(newValue), oldValue was \(i)") } func didSetI(oldValue: Int) { print("didSet to \(i), oldValue was \(oldValue)") } var i: Int { get { return _i } set { willSetI(newValue: newValue) let oldValue = _i _i = newValue didSetI(oldValue: oldValue) } }
Неоконченное сохраненное свойство в классах проблематично, поскольку оно может быть переопределено вычисляемым свойством.
И это даже не рассматривает случаи, которые зависят от деталей реализации внутри компилятора.
По этой причине компилятор гарантирует только стабильные и уникальные значения указателя из преобразований вход-в-указатель для хранимых глобальных и статических хранимых переменных без наблюдателей. В любом другом случае попытка сбежать и использовать указатель из преобразования inout-to-pointer после вызова, которому он был передан, приведет к неопределенному поведению.
Хорошо, но как мой пример с функцией foo
относятся к вашему примеру вызова UnsafeMutablePointer
Инициализатора? Что ж, UnsafeMutablePointer
имеет инициализатор, который принимает UnsafeMutablePointer
аргумент (в результате соответствия подчеркнутым _Pointer
протокол, которому соответствует большинство стандартных типов указателей библиотеки).
Этот инициализатор фактически такой же, как foo
функция - это занимает UnsafeMutablePointer
аргумент и возвращает его. Поэтому, когда вы делаете UnsafeMutablePointer(&intObservingThing.anInt)
вы экранируете указатель, полученный в результате преобразования inout-to-pointer - что, как мы уже обсуждали, допустимо только в том случае, если оно используется для хранимой глобальной или статической переменной без наблюдателей.
Итак, чтобы обернуть вещи:
var intObservingThing = IntObservingThing(anInt: 0)
var otherPtr = UnsafeMutablePointer(&intObservingThing.anInt)
// "I was just set to 0."
otherPtr.pointee = 20
является неопределенным поведением. Указатель, полученный в результате преобразования inout-to-pointer, действителен только на время вызова UnsafeMutablePointer
инициализатор. Попытка использовать его впоследствии приводит к неопределенному поведению. Как показывает Мэтт, если вы хотите получить доступ к указателю в intObservingThing.anInt
, вы хотите использовать withUnsafeMutablePointer(to:)
,
На самом деле я сейчас работаю над реализацией предупреждения (которое, мы надеемся, перейдем к ошибке), которое будет генерироваться при таких неправильных преобразованиях in-to-pointer. К сожалению, в последнее время у меня не было много времени, чтобы работать над этим, но все идет хорошо, я собираюсь начать продвигать его вперед в новом году и, надеюсь, включить его в выпуск Swift 5.x.
Кроме того, стоит отметить, что хотя компилятор в настоящее время не гарантирует четко определенного поведения для:
var normalThing = NormalThing(anInt: 0)
var ptr = UnsafeMutablePointer(&normalThing.anInt)
ptr.pointee = 20
Из обсуждения # 20467, похоже, что это будет то, что компилятор гарантирует четко определенное поведение в будущем выпуске, из-за того, что база (normalThing
) является хрупкой хранимой глобальной переменной struct
без наблюдателей и anInt
является хрупким хранимым свойством без наблюдателей.
Я уверен, что проблема в том, что то, что ты делаешь, незаконно. Вы не можете просто объявить небезопасный указатель и заявить, что он указывает на адрес свойства struct. (На самом деле, я даже не понимаю, почему ваш код компилируется в первую очередь; какой инициализатор считает, что это компилятор?) Правильный путь, который дает ожидаемые результаты, - это запросить указатель, который указывает на это. адрес, как это:
struct IntObservingThing {
var anInt: Int = 0 {
didSet {
print("I was just set to \(anInt)")
}
}
}
withUnsafeMutablePointer(to: &intObservingThing.anInt) { ptr -> Void in
ptr.pointee = 20 // I was just set to 20
}
print(intObservingThing.anInt) // 20