在我创建的结构(在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操作。我在这里想念什么?
答案 0 :(得分:6)
每当您看到类似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
的设置器。有效地执行以下操作:
var j = i // calling `i`'s getter
foo(&j)
i = j // calling `i`'s setter
从此示例中应该可以清楚地看出,这对传递给foo
的指针的生存期施加了重要的限制–它只能用于在调用过程中使i
的值发生变化。 foo
。尝试转义指针并在调用foo
之后使用它会导致 only 修改临时变量的值,而不是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)
}
}
类上的非最终存储属性存在问题,因为它可以被计算属性覆盖。
这甚至都没有考虑依赖编译器中实现细节的情况。
由于这个原因,编译器仅从内部到外部的转换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到指针的转换产生的指针-正如我们已经讨论过的,只有在没有观察者的情况下将其用于存储的全局或静态变量时,该指针才有效。
因此,总结一下:
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到in-pointer转换将发出此信号。不幸的是,我最近没有太多时间来处理它,但是一切进展顺利,我的目标是在新的一年中将其推向前进,并希望将其纳入Swift 5.x发行版中。
此外,值得注意的是,尽管编译器当前不能保证以下行为的明确定义:
var normalThing = NormalThing(anInt: 0)
var ptr = UnsafeMutablePointer(&normalThing.anInt)
ptr.pointee = 20
从#20467的讨论来看,由于基本的事实,这似乎很可能使编译器在将来的版本中保证良好定义的行为。 (normalThing
)是没有观察者的struct
的易碎存储全局变量,而anInt
是没有观察者的易碎的存储属性。
答案 1 :(得分:3)
我非常确定问题是您的举动是非法的。您不能只是声明一个不安全的指针并声称它指向struct属性的地址。 (实际上,我什至不明白为什么您的代码首先会编译;编译器认为这是什么初始化器?)给出预期结果的正确方法是 ask 指向指向该地址的指针,如下所示:
library(dplyr)
library(tidyr)
myframe %>%
tidyr::gather(Znum, Zval, -id) %>%
arrange(-Zval) %>%
group_by(id) %>%
slice(1:3) %>%
ungroup()
# # A tibble: 30 x 3
# id Znum Zval
# <int> <chr> <int>
# 1 101 Z5 88
# 2 101 Z7 79
# 3 101 Z6 72
# 4 102 Z6 95
# 5 102 Z4 74
# 6 102 Z1 70
# 7 103 Z5 81
# 8 103 Z10 69
# 9 103 Z2 67
# 10 104 Z7 98
# # ... with 20 more rows