swift - UnsafeMutablePointer.pointee和didSet属性

标签 swift pointers unsafemutablepointer

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

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操作。我在这里想念什么?

最佳答案

每当您看到类似UnsafeMutablePointer(&intObservingThing.anInt)的结构时,都应该非常警惕它是否会表现出不确定的行为。在绝大多数情况下,它会。

首先,让我们详细分析一下这里发生的事情。 UnsafeMutablePointer没有任何采用inout参数的初始化程序,那么此调用程序是什么?好的,编译器有一个特殊的转换,允许将&前缀的参数转换为指向表达式引用的“存储”的可变指针。这称为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的getter,并将结果值放入一个临时变量中。
  • 获取指向该临时变量的指针,并将其传递给foo的调用。
  • 使用临时值中的新值调用i的setter。

  • 有效地执行以下操作:
    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
    }
    
  • 与观察者一起存储的变量是有问题的,因为在幕后它实际上被实现为在其setter中调用其观察者的计算变量。

    例如:
    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)
      }
    }
    
  • 类上的非最终存储属性存在问题,因为它可以被计算属性覆盖。

  • 而且,这甚至没有考虑依赖编译器中实现细节的情况。

    因此,仅编译器可确保int到指针的转换on stored global and static stored variables without observers中的稳定且唯一的指针值。在任何其他情况下,尝试在传递了调用的调用后转义并使用inout-to-pointer转换中的指针将导致未定义的行为。

    好的,但是带有函数foo的示例与您调用UnsafeMutablePointer初始化程序的示例有什么关系?好吧,UnsafeMutablePointer has an initialiser that takes an UnsafeMutablePointer argument(作为符合大多数标准库指针类型所遵循的强调的_Pointer协议(protocol)的结果)。

    该初始化程序实际上与foo函数相同–它接受UnsafeMutablePointer参数并返回它。因此,当您执行UnsafeMutablePointer(&intObservingThing.anInt)时,就转义了从inout到指针的转换产生的指针-正如我们已经讨论的那样,仅当它用于没有观察者的存储的全局或静态变量时才有效。

    因此,总结一下:
    var intObservingThing = IntObservingThing(anInt: 0)
    var otherPtr = UnsafeMutablePointer(&intObservingThing.anInt)
    // "I was just set to 0."
    
    otherPtr.pointee = 20
    

    是不确定的行为。从inout到指针的转换产生的指针仅在对UnsafeMutablePointer的初始化程序的调用期间有效。之后尝试使用它会导致不确定的行为。作为matt demonstrates,如果要对intObservingThing.anInt进行范围内的指针访问,则要使用withUnsafeMutablePointer(to:)

    I'm actually currently working on implementing a warning(希望将转换为错误)将在这种不正确的inout到指针转换中发出。不幸的是,我最近没有太多时间来处理它,但是一切进展顺利,我的目标是在新的一年中开始 push 它的发展,并希望将其纳入Swift 5.x版本。

    另外,值得注意的是,尽管编译器当前不保证以下行为的定义良好:
    var normalThing = NormalThing(anInt: 0)
    var ptr = UnsafeMutablePointer(&normalThing.anInt)
    ptr.pointee = 20
    

    从对#20467的讨论来看,由于基本(normalThing)是struct的易碎存储全局变量而没有观察者,因此在将来的发行版中,编译器很可能会保证为这种行为定义良好的行为。 ,而anInt是没有观察者的易碎存储属性。

    关于swift - UnsafeMutablePointer.pointee和didSet属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53892190/

    相关文章:

    java - 更改java中LinkedList中元素的值

    ios - 如何通过 voip 和调用套件处理视频调用

    ios - Swift:检测非字母数字字符的正则表达式

    swift - 在准备 segue 时找不到 `ViewController`

    c - 动态内存分配 - realloc() : invalid next size

    ios - 如何使用 "getControlPoint(at idx: Int, values ptr: UnsafeMutablePointer<Float>!)"CAMediaTimingFunction

    swift - 防止UIButton长按重复功能

    不持有有效地址的 C 程序指针仍然没有给出段错误

    ios - 将代码从 Objective-C 转换为 Swift 后测试用例失败

    swift - Swift 中的指针