swift – UnsafeMutablePointer.pointee和didSet属性
我在我创建的结构中的一个观察属性上使用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,并将结果值放入临时变量中. 有效地做到以下几点: 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 这个初始化器与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是一个没有观察者的脆弱的存储属性. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |