UnsafeMutablePointer.pointee和didSet属性

kid*_*d_x 4 pointers swift unsafemutablepointer

我在我创建的结构中的一个观察属性上使用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"
Run Code Online (Sandbox Code Playgroud)

看起来,将UnsafeMutablePointer上的指针修改为观察到的属性实际上并不会修改属性的值.此外,将指针指向属性的操作会触发didSet操作.我在这里错过了什么?

Ham*_*ish 6

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

首先,让我们分解一下这里发生的事情.UnsafeMutablePointer没有任何带inout参数的初始化者,那么这个调用是什么初始化者?好吧,编译器有一个特殊的转换,允许将&前缀参数转换为指向表达式引用的"存储"的可变指针.这称为inout-to-pointer转换.

例如:

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

var i = 0
foo(&i)
print(i) // 1
Run Code Online (Sandbox Code Playgroud)

编译器插入一个转换&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
Run Code Online (Sandbox Code Playgroud)

这仍然有效,那么指针指向什么存储?要解决这个问题,编译器:

  1. 调用igetter,并将结果值放入临时变量中.
  2. 获取指向该临时变量的指针,并将其传递给调用foo.
  3. i用临时的新值调用setter.

有效地做到以下几点:

var j = i // calling `i`'s getter
foo(&j)
i = j     // calling `i`'s setter
Run Code Online (Sandbox Code Playgroud)

从这个例子中可以清楚地看出,这对传递给指针的生命周期施加了一个重要的约束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
Run Code Online (Sandbox Code Playgroud)

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
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 带有观察者的存储变量是有问题的,因为它实际上是作为一个计算变量实现的,它在其setter中调用它的观察者.

    例如:

    var i: Int = 0 {
      willSet(newValue) {
        print("willSet to \(newValue), oldValue was \(i)")
      }
      didSet(oldValue) {
        print("didSet to \(i), oldValue was \(oldValue)")
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    基本上是语法糖:

    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)
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 类上的非最终存储属性是有问题的,因为它可以被计算属性覆盖.

这甚至不考虑依赖于编译器中的实现细节的情况.

因此,编译器只保证在没有观察者的情况下,对存储的全局和静态存储变量的 inout-to-pointer转换的稳定和唯一指针值.在任何其他情况下,尝试转义并在传递给它之后使用指针从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
Run Code Online (Sandbox Code Playgroud)

是未定义的行为.从in-to-pointer转换产生的指针仅在调用UnsafeMutablePointer初始化器的持续时间内有效.之后尝试使用它会导致未定义的行为.正如matt演示的那样,如果你想要使用范围指针intObservingThing.anInt,你想要使用withUnsafeMutablePointer(to:).

我实际上正在实现一个警告(希望转换为错误),这个警告将在这种不合理的inout-to-pointer转换中发出.不幸的是,我最近没有多少时间来处理它,但是一切顺利,我的目标是在新的一年里开始推进它,并希望将它变成Swift 5.x版本.

此外,值得注意的是,虽然编译器目前不能保证定义良好的行为:

var normalThing = NormalThing(anInt: 0)
var ptr = UnsafeMutablePointer(&normalThing.anInt)
ptr.pointee = 20
Run Code Online (Sandbox Code Playgroud)

从上讨论#20467,它看起来像这可能是东西,编译器保证良好定义的行为在以后的版本中,由于这样的事实基础(normalThing)是一个脆弱的存储全局变量struct没有观察员,并且anInt是一个没有观察者的脆弱的存储财产.