【问题标题】:UnsafeMutablePointer.pointee and didSet propertiesUnsafeMutablePointer.pointee 和 didSet 属性
【发布时间】:2019-05-22 08:42:57
【问题描述】:

我在我创建的结构(在 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 动作。我在这里错过了什么?

【问题讨论】:

    标签: swift pointers unsafemutablepointer


    【解决方案1】:

    任何时候你看到像UnsafeMutablePointer(&intObservingThing.anInt) 这样的构造,你应该非常警惕它是否会表现出未定义的行为。在绝大多数情况下,它会。

    首先,让我们详细分析一下这里发生了什么。 UnsafeMutablePointer 没有任何带有 inout 参数的初始化程序,那么这个调用是什么初始化程序?好吧,编译器有一个特殊的转换,它允许将 & 前缀参数转换为指向表达式所引用的“存储”的可变指针。这称为输入输出到指针的转换。

    例如:

    func foo(_ ptr: UnsafeMutablePointer<Int>) {
      ptr.pointee += 1
    }
    
    var i = 0
    foo(&i)
    print(i) // 1
    

    编译器插入一个转换,将&amp;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 的指针的生命周期施加了一个重要的限制——它只能用于在调用foo 期间改变i 的值。尝试转义指针并在调用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 的调用结束后仍然有效。例如,优化器可以在调用后立即取消初始化。

    好的,但是只要我们只获得指向未计算变量的指针,我们应该能够在它被传递到的调用之外使用指针,对吧?不幸的是,事实证明,在逃避 inout 到指针的转换时,还有很多其他的方法可以让自己在脚上开枪!

    仅举几例(还有更多!):

    • 一个局部变量是有问题的,原因与我们之前的临时变量类似——编译器不保证它会保持初始化直到它声明的范围结束。优化器可以更早地取消初始化它.

      例如:

      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 到指针转换 on stored global and static stored variables without observers 的稳定且唯一的指针值。在任何其他情况下,尝试在传递调用后从 inout 到指针转换中转义并使用指针将导致 未定义的行为


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

    这个初始化器实际上与foo 函数相同——它接受一个UnsafeMutablePointer 参数并返回它。因此,当您执行UnsafeMutablePointer(&amp;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 到指针转换时发出。不幸的是,我最近没有太多时间来研究它,但一切都很顺利,我的目标是在新的一年里开始推动它,并希望将它放入 Swift 5.x 版本中。

    此外,值得注意的是,虽然编译器目前不保证以下行为的明确定义:

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

    #20467 上的讨论来看,这似乎可能是编译器确实保证在未来版本中明确定义的行为,因为基础 (@ 987654364@) 是没有观察者的struct 的脆弱存储全局变量,anInt 是没有观察者的脆弱存储属性。

    【讨论】:

    • 是的,我感觉到它与指针生命周期和未定义的行为有关,但我无法制定实际的概念。我也知道你一直在研究指针,这就是我在 bugs.swift.org 上标记它的原因!
    【解决方案2】:

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

    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
    

    【讨论】:

    • “你不能只声明一个不安全的指针并声称它指向一个结构属性的地址。” - 你能指出任何说明这一点的文件吗?不挑战它,但我想看看这句话背后的原因。
    • 关于编译器认为这是什么初始化程序 - 为什么它会认为它不是 UnsafeMutablePointer 的初始化程序?
    • 我们这样看。你写UnsafeMutablePointer(&amp;intObservingThing.anInt)。现在,如果这是合法的,它应该可以与UnsafeMutablePointer.init(&amp;intObservingThing.anInt) 互换。但是如果你这样写,编译器就会窒息——这是正确的。所以我认为你的表达,由于编译器中的一些错误,正在悄悄地越过编译器的保护。我承认我有点模糊,但我确实认为我已经解释了如何正确地做到这一点,并以这样的方式获得预期的结果。
    • 绝对可以这样工作。这是否意味着裸 UnsafeMutablePointers 不应该被显式声明/初始化? IE。它们将始终是某些函数中的参数?
    • 不,我没这么说。
    猜你喜欢
    • 2019-12-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-11
    • 1970-01-01
    • 1970-01-01
    • 2014-10-22
    相关资源
    最近更新 更多