以下代码可以在Swift Playground中运行:
import UIKit
func aaa(_ key: UnsafeRawPointer!, _ value: Any! = nil) {
print(key)
}
func bbb(_ key: UnsafeRawPointer!) {
print(key)
}
class A {
var key = "aaa"
}
let a = A()
aaa(&a.key)
bbb(&a.key)
这是我的mac上打印的结果:
0x00007fff5dce9248
0x00007fff5dce9220
为什么两个打印的结果不同?更有趣的是,当我更改 bbb 的功能签名以使其与 aaa 相同时,两次打印的结果是相同的。如果我在这两个函数调用中使用全局变量而不是 a.key ,则两次打印的结果是相同的。有谁知道为什么会发生这种奇怪的行为?
答案 0 :(得分:4)
为什么两次打印的结果不同?
因为对于每个函数调用,Swift is creating a temporary variable初始化为a.key
的getter返回的值。使用指向其给定临时变量的指针调用每个函数。因此,指针值可能不一样 - 因为它们引用不同的变量。
这里使用临时变量的原因是因为A
是一个非final类,因此可以通过子类使其key
重写的getter和setter(这可以很好地将其重新实现为计算属性。)
因此,在未优化的构建中,编译器不能直接将key
的地址传递给函数,而是必须依赖于调用getter(尽管在优化构建中,此行为可以完全改变)。
您需要注意的是,如果您将key
标记为final
,您现在应该在两个函数中获得一致的指针值:
class A {
final var key = "aaa"
}
var a = A()
aaa(&a.key) // 0x0000000100a0abe0
bbb(&a.key) // 0x0000000100a0abe0
因为现在key
的地址只能directly passed to the functions,完全绕过它的吸气剂。
值得注意的是,一般来说,不应该依赖此行为。您在函数中获得的指针值是纯粹的实现细节,不保证稳定。编译器可以随意调用它们所希望的函数,只是向你保证你得到的指针在调用期间是有效的,并且会将pointees初始化为期望的值(如果是可变的,你对它做出的任何更改)调用者将会看到他们。
此规则的仅异常是指向全局和静态存储变量的指针传递。 Swift 确保保证您获得的指针值对于该特定变量是稳定且唯一的。来自Swift团队的blog post on Interacting with C Pointers(强调我的):
然而,与C指针的交互本质上是 与其他Swift代码相比,不安全,因此必须小心。在 特别是:
- 如果被叫方无法安全使用这些转换 保存指针值,以便在返回后使用。指针那个 这些转化的结果仅保证对此有效 通话时间。即使你传递相同的变量,数组,或 字符串作为多个指针参数,您可以收到不同的 指针每次。 此例外是全局或静态存储 变量。您可以安全地使用全局变量的地址作为 持久性唯一指针值,例如:作为KVO上下文参数。
因此,如果您使key
成为A
的静态存储属性或仅仅是全局存储变量,则可以保证在两个函数调用中都获得相同的指针值。
当我更改
bbb
的功能签名以使其与aaa
相同时,两次打印的结果相同
这似乎是一种优化,因为我只能在-O版本和游乐场中重现它。在未优化的构建中,添加或删除额外参数不起作用。
(虽然值得注意的是你不应该在游乐场中测试Swift行为,因为它们不是真正的Swift环境,并且可以对使用swiftc
编译的代码展示不同的运行时行为)
这种行为的原因仅仅是巧合 - 第二个临时变量能够驻留在与第一个相同的相同的地址(在第一个被解除分配之后)。当您向aaa
添加额外参数时,将在'之间分配一个新变量。它们保存要传递的参数值,防止它们共享相同的地址。
由于a
的中间负载,为了调用getter获取a.key
的值,在未优化的构建中无法观察到相同的地址。作为优化,如果编译器具有带常量表达式的属性初始化器,则编译器能够将a.key
的值内联到调用站点,从而消除了对此中间负载的需要。
因此,如果您给a.key
一个非确定的值,例如var key = arc4random()
,那么您应该再次观察不同的指针值,因为a.key
的值不能再内联。
但不管原因如何,这是一个完美的示例,说明变量(非全局或静态存储变量)的指针值如何不依赖on - 因为您获得的值可以根据优化级别和参数计数等因素完全改变。
inout
& UnsafeMutable(Raw)Pointer
关于your comment:
但是因为
withUnsafePointer(to:_:)
总是具有我想要的正确行为(事实上它应该,否则这个函数是没用的),并且它还有一个inout
参数。所以我假设这些函数与inout
参数之间存在实现差异。
编译器以{em>略微的方式将inout
参数视为UnsafeRawPointer
参数。这是因为您可以在函数调用中改变inout
参数的值,但不能改变pointee
的{{1}}。
为了使调用者可以看到UnsafeRawPointer
参数值的任何突变,编译器通常有两个选项:
将临时变量初始化为变量“getter”返回的值。使用指向此变量的指针调用该函数,并在函数返回后,使用临时变量的(可能已突变的)值调用变量的setter。
如果它是可寻址的,只需使用 direct 指向变量的指针调用该函数。
如上所述,编译器不能将第二个选项用于不知道为inout
的存储属性(但这可以随着优化而改变)。但是,对于大值,始终依赖第一个选项可能会非常昂贵,因为它们必须被复制。对于具有写时复制行为的值类型,这尤其是有害的,因为它们依赖于唯一的,以便对其底层缓冲区执行直接变换 - 临时副本违反了这一点。
为了解决这个问题,Swift实现了一个特殊的访问器 - 名为materializeForSet
。此访问器允许被调用者向调用者提供指向给定变量的 direct 指针(如果它是可寻址的),否则将返回指向包含该变量副本的临时缓冲区的指针,在使用之后需要将其写回设定者。
前者是您从final
返回inout
- you're getting a direct pointer到a.key
的行为,因此您在两个函数调用中都会获得指针值是一样的。
但是,materializeForSet
仅用于需要回写的函数参数,这解释了为什么它不用于materializeForSet
。如果您将UnsafeRawPointer
和aaa
的函数参数设为bbb
s(其中执行需要回写),则应再次观察相同的指针值。
UnsafeMutable(Raw)Pointer
但是,如上所述,对于非全局或静态的变量,此行为不依赖。