Swift Unsafemeutablepointer.pointee和didSet属性

Swift Unsafemeutablepointer.pointee和didSet属性,swift,pointers,unsafemutablepointer,Swift,Pointers,Unsafemutablepointer,在我创建的结构(在Xcode 10.1、Swift 4.2上)中观察到的属性上使用UnsafemeutablePointer,我得到了一些意外的行为。请参见以下游乐场代码: struct NormalThing { var anInt = 0 } struct IntObservingThing { var anInt: Int = 0 { didSet { print("I was just set to \(anInt)")

在我创建的结构(在Xcode 10.1、Swift 4.2上)中观察到的属性上使用UnsafemeutablePointer,我得到了一些意外的行为。请参见以下游乐场代码:

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"

从表面上看,在指向观察到的属性的UnsafemeutablePointer上修改指针对象实际上并没有修改属性的值。此外,将指针指定给属性的行为将触发didSet操作。我在这里遗漏了什么?

我很确定问题在于你所做的是非法的。您不能仅仅声明一个不安全的指针并声称它指向一个结构属性的地址。(事实上,我甚至不明白为什么您的代码首先要编译;编译器认为这是什么初始值设定项?)给出预期结果的正确方法是请求指向该地址的指针,如下所示:

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

每当您看到像
unsafemeutablepointer(&intobservingting.anInt)
这样的构造时,您都应该非常小心它是否会表现出未定义的行为。在绝大多数情况下,它会

首先,让我们详细分析一下这里发生的事情
UnsafemeutablePointer
没有任何接受
inout
参数的初始化器,那么此调用的是哪个初始化器?好的,编译器有一个特殊的转换,它允许
&
前缀参数转换为指向表达式引用的“存储”的可变指针。这称为inout到指针的转换

例如:

func foo(_ ptr: UnsafeMutablePointer<Int>) {
  ptr.pointee += 1
}

var i = 0
foo(&i)
print(i) // 1
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
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)")
  }
}
这仍然有效,那么指针指向的存储是什么呢?要解决此问题,编译器:

  • 调用
    i
    的getter,并将结果值放入临时变量中
  • 获取指向该临时变量的指针,并将其传递给对
    foo
    的调用
  • 使用临时变量中的新值调用
    i
    的setter
  • 有效地做到以下几点:

    var j = i // calling `i`'s getter
    foo(&j)
    i = j     // calling `i`'s setter
    
    希望从这个例子中可以清楚地看到,这对传递到
    foo
    的指针的生命周期施加了一个重要的约束–它只能用于在调用
    foo
    期间改变
    i
    的值。尝试转义指针并在调用
    foo
    后使用它将导致仅修改临时变量的值,而不会修改
    i

    例如:

    func foo(_ ptr: UnsafeMutablePointer<Int>) {
      ptr.pointee += 1
    }
    
    var i = 0
    foo(&i)
    print(i) // 1
    
    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
    
    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)")
      }
    }
    
  • 带有观察者的存储变量是有问题的,因为它实际上是作为一个计算变量实现的,在setter中调用它的观察者

    例如:

    func foo(_ ptr: UnsafeMutablePointer<Int>) {
      ptr.pointee += 1
    }
    
    var i = 0
    foo(&i)
    print(i) // 1
    
    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
    
    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到指针转换的稳定且唯一的指针值。在任何其他情况下,在调用传递到inout之后,尝试从inout到指针转换转义并使用指针将导致未定义的行为


    好的,但是我的函数
    foo
    的示例与调用
    unsafemeutablepointer
    初始化器的示例有什么关系呢?嗯,
    unsafemeutablepointer
    (由于符合大多数标准库指针类型所遵循的带下划线的
    \u-Pointer
    协议)

    此初始化器实际上与
    foo
    函数相同–它接受
    unsafemeutablepointer
    参数并返回它。因此,当您执行
    unsafemeutablepointer(&intobservingting.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
    的初始化器期间有效。之后尝试使用它会导致未定义的行为。同样,如果您希望对
    的作用域指针访问进入bservingting.anInt
    ,您希望使用
    和不可配置指针(to:)

    (这很可能会转变为一个错误)在这种不可靠的inout-to-pointer转换中会发出。不幸的是,我最近没有太多的时间来研究它,但一切进展顺利,我的目标是在新的一年里开始推动它向前发展,并希望它能在Swift 5.x版本中发布

    此外,值得注意的是,尽管编译器目前无法保证以下各项的良好定义行为:

    var normalThing = NormalThing(anInt: 0)
    var ptr = UnsafeMutablePointer(&normalThing.anInt)
    ptr.pointee = 20
    

    从上面的讨论来看,这很可能是编译器在未来版本中保证良好定义的行为的东西,因为基本(
    normalThing
    )是一个不带观察者的
    struct
    的脆弱存储全局变量,而
    anInt
    是一个脆弱的存储属性,没有观察者。

    “你不能仅仅声明一个不安全的指针并声称它指向一个结构属性的地址。”-你能指出任何说明这一点的文档吗?我不想挑战它,但我想看看这条语句背后的原因。关于编译器认为这是什么样的初始值设定项-为什么它认为这不是指向UnsafemtablePointer的初始值设定项?让我们这样看。您可以编写
    不可配置的指针(&intobservingting.anInt)
    。现在,如果这是合法的,它应该可以与
    UnsafeMutablePointer.init(&intobservingting.anInt)
    互换。但是如果你这样写的话,编译器就会阻塞——这是正确的。因此,我认为您的表达式,由于编译器中的一些错误,正在悄悄地通过编译器的保护。我承认我有点含糊不清,但我确实认为我已经解释了h