Свойства UnsafeMutablePointer.pointee и didSet - PullRequest
0 голосов
/ 22 декабря 2018

Я получил неожиданное поведение при использовании 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 ]

0 голосов
/ 22 декабря 2018

Каждый раз, когда вы видите такую ​​конструкцию, как 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

Это все еще работает, так на какое хранилище указывает указатель?Чтобы решить эту проблему, компилятор:

  1. Вызывает геттер i и помещает результирующее значение во временную переменную.
  2. Получает указатель на эту временную переменную ипередает это вызову foo.
  3. Вызывает установщик i с новым значением из временного.

Эффективно выполняя следующее:

var j = i // calling `i`'s getter
foo(&j)
i = j     // calling `i`'s setter

Надеемся, что из этого примера должно быть ясно, что это накладывает важное ограничение на время жизни указателя, переданного в foo - его можно использовать только для изменения значения i во время вызова foo,Попытка экранировать указатель и использовать его после вызова foo приведет к изменению only значения временной переменной, а не 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 был вызван с новым значением временной переменной, поэтому он не имеет никакого эффекта.

Хуже этого, он демонстрирует неопределенное поведение , так как компилятор не гарантирует, что временная переменная останется действительной после завершения вызова 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 - хрупкое хранимое свойство без наблюдателей.

0 голосов
/ 22 декабря 2018

Я почти уверен, что проблема в том, что вы делаете незаконно.Вы не можете просто объявить небезопасный указатель и заявить, что он указывает на адрес свойства 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
...