在我创建的结构中(在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操作。我在这里想念什么?
最佳答案
每当您看到类似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
这仍然有效,因此指针指向的是什么存储空间?为了解决这个问题,编译器:
i
的getter,并将结果值放入一个临时变量中。 foo
的调用。 i
的setter。 有效地执行以下操作:
var j = i // calling `i`'s getter
foo(&j)
i = j // calling `i`'s setter
希望从该示例可以清楚地看出,这对传递给
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
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)
}
}
而且,这甚至没有考虑依赖编译器中实现细节的情况。
因此,仅编译器可确保int到指针的转换on stored global and static stored variables without observers中的稳定且唯一的指针值。在任何其他情况下,尝试在传递了调用的调用后转义并使用inout-to-pointer转换中的指针将导致未定义的行为。
好的,但是带有函数
foo
的示例与您调用UnsafeMutablePointer
初始化程序的示例有什么关系?好吧,UnsafeMutablePointer
has an initialiser that takes an UnsafeMutablePointer
argument(作为符合大多数标准库指针类型所遵循的强调的_Pointer
协议(protocol)的结果)。该初始化程序实际上与
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到指针转换中发出。不幸的是,我最近没有太多时间来处理它,但是一切进展顺利,我的目标是在新的一年中开始 push 它的发展,并希望将其纳入Swift 5.x版本。
另外,值得注意的是,尽管编译器当前不保证以下行为的定义良好:
var normalThing = NormalThing(anInt: 0)
var ptr = UnsafeMutablePointer(&normalThing.anInt)
ptr.pointee = 20
从对#20467的讨论来看,由于基本(
normalThing
)是struct
的易碎存储全局变量而没有观察者,因此在将来的发行版中,编译器很可能会保证为这种行为定义良好的行为。 ,而anInt
是没有观察者的易碎存储属性。
关于swift - UnsafeMutablePointer.pointee和didSet属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53892190/