每当您看到类似 的构造时UnsafeMutablePointer(&intObservingThing.anInt)
,您都应该非常警惕它是否会表现出未定义的行为。在绝大多数情况下,它会。
首先,让我们准确地分解这里发生的事情。UnsafeMutablePointer
没有任何带inout
参数的初始化程序,那么这个调用是什么初始化程序?好吧,编译器有一个特殊的转换,它允许将带&
前缀的参数转换为指向表达式所引用的“存储”的可变指针。这称为输入输出到指针的转换。
例如:
func foo(_ ptr: UnsafeMutablePointer<Int>) {
ptr.pointee += 1
}
var i = 0
foo(&i)
print(i) // 1
编译器插入一个转换为指向's 存储&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
在使用临时变量的新值调用 's setter之后 发生i
,因此它没有效果。
更糟糕的是,它表现出未定义的行为,因为编译器不保证临时变量在调用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 到指针转换的稳定和唯一的指针值。在任何其他情况下,在将指针传递给的调用之后,尝试从 inout 到指针的转换中转义并使用指针将导致未定义的行为。
好的,但是我的函数示例与foo
您调用UnsafeMutablePointer
初始化程序的示例有何关系?好吧,UnsafeMutablePointer
有一个带有UnsafeMutablePointer
参数的初始化程序(作为符合_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 所演示的,如果您想要范围内的指针访问intObservingThing.anInt
,您想要使用withUnsafeMutablePointer(to:)
.
实际上,我目前正在实施一个警告(希望会转变为错误),该警告将在这种不健全的 inout 到指针转换时发出。不幸的是,我最近没有太多时间来研究它,但一切都很顺利,我的目标是在新的一年里开始推动它,并希望将它放入 Swift 5.x 版本中。
此外,值得注意的是,虽然编译器目前不保证以下方面的良好定义行为:
var normalThing = NormalThing(anInt: 0)
var ptr = UnsafeMutablePointer(&normalThing.anInt)
ptr.pointee = 20
从#20467的讨论来看,这很可能是编译器在未来版本中保证明确定义的行为,因为 base ( normalThing
) 是一个struct
没有观察者的脆弱的存储全局变量,并且anInt
是一个没有观察者的脆弱的存储财产。