加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

swift – UnsafeMutablePointer.pointee和didSet属性

发布时间:2020-12-14 04:48:45 所属栏目:百科 来源:网络整理
导读:我在我创建的结构中的一个观察属性上使用UnsafeMutablePointer得到了一些意想不到的行为(在 Xcode 10.1,Swift 4.2上).请参阅以下游乐场代码: struct NormalThing { var anInt = 0}struct IntObservingThing { var anInt: Int = 0 { didSet { print("I was j
我在我创建的结构中的一个观察属性上使用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) // "20n"

var intObservingThing = IntObservingThing(anInt: 0)
var otherPtr = UnsafeMutablePointer(&intObservingThing.anInt)
// "I was just set to 0."

otherPtr.pointee = 20
print(intObservingThing.anInt) // "0n"

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

解决方法

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

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

例如:

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

var i = 0
foo(&i)
print(i) // 1

编译器插入一个转换,将& 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

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

>调用我的getter,并将结果值放入临时变量中.
>获取指向该临时变量的指针,并将其传递给foo调用.
>使用临时值中的新值调用我的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>) -> 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发生在使用临时变量的新值调用了我的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)
  }
}

>类上的非最终存储属性是有问题的,因为它可以被计算属性覆盖.

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

由于这个原因,编译器只保证来自inout-to-pointer转换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-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的初始化程序期间有效.之后尝试使用它会导致未定义的行为.如matt demonstrates所示,如果您希望使用scoped指针访问intObservingThing.anInt,则需要使用withUnsafeMutablePointer(to :).

I’m actually currently working on implementing a warning(希望转换为错误)将在这种不合理的inout-to-pointer转换中发出.不幸的是,我最近没有多少时间来处理它,但是一切顺利,我的目标是在新的一年里开始推进它,并希望将它变成Swift 5.x版本.

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

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

从#20467的讨论看来,由于base(normalThing)是没有观察者的结构的脆弱存储全局变量,因此编译器确实可以保证在将来的版本中有明确定义的行为.和anInt是一个没有观察者的脆弱的存储属性.

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读