UnsafeMutablePointer.pointee和didSet属性

时间:2018-12-22 00:37:33

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

2 个答案:

答案 0 :(得分:6)

每当您看到类似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

这仍然有效,因此指针指向什么存储空间?为了解决这个问题,编译器:

  1. 调用i的getter,并将结果值放入一个临时变量。
  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用临时变量的新值调用了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)
      }
    }
    
  • 类上的非最终存储属性存在问题,因为它可以被计算属性覆盖。

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

由于这个原因,编译器仅从内部到外部的转换on stored global and static stored variables without observers保证稳定且唯一的指针值。在任何其他情况下,尝试在传递调用后转义并使用inout-to-pointer转换中的指针将导致不确定的行为


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

此初始化程序实际上与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到in-pointer转换将发出此信号。不幸的是,我最近没有太多时间来处理它,但是一切进展顺利,我的目标是在新的一年中将其推向前进,并希望将其纳入Swift 5.x发行版中。

此外,值得注意的是,尽管编译器当前不能保证以下行为的明确定义:

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

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

答案 1 :(得分:3)

我非常确定问题是您的举动是非法的。您不能只是声明一个不安全的指针并声称它指向struct属性的地址。 (实际上,我什至不明白为什么您的代码首先会编译;编译器认为这是什么初始化器?)给出预期结果的正确方法是 ask 指向指向该地址的指针,如下所示:

library(dplyr)
library(tidyr)

myframe %>%
  tidyr::gather(Znum, Zval, -id) %>%
  arrange(-Zval) %>%
  group_by(id) %>%
  slice(1:3) %>%
  ungroup()
# # A tibble: 30 x 3
#       id Znum   Zval
#    <int> <chr> <int>
#  1   101 Z5       88
#  2   101 Z7       79
#  3   101 Z6       72
#  4   102 Z6       95
#  5   102 Z4       74
#  6   102 Z1       70
#  7   103 Z5       81
#  8   103 Z10      69
#  9   103 Z2       67
# 10   104 Z7       98
# # ... with 20 more rows