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相同时,两次打印的结果是相同的.如果我在这两个函数调用中使用全局var而不是a.key,则两次打印的结果是相同的.有谁知道为什么会发生这种奇怪的行为?
Why the results of two prints differs?
因为对于每个函数调用,Swift is creating a temporary variable初始化为a.key的getter返回的值.使用指向其给定临时变量的指针调用每个函数.因此指针值可能不一样 – 因为它们引用不同的变量.
这里使用临时变量的原因是因为A是非final类,因此可以使其子类的getter和setter重写(可以将其重新实现为计算属性).
因此,在未优化的构建中,编译器不能直接将键的地址直接传递给函数,而是必须依赖于调用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,完全绕过它的getter.
值得注意的是,一般来说,你不应该依赖这种行为.您在函数中获得的指针值是纯粹的实现细节,并不保证是稳定的.编译器可以根据自己的意愿自由调用函数,只承诺你获得的指针在调用期间有效,并且会将pointees初始化为期望值(如果是可变的,则对你所做的任何更改)调用者将会看到调查员.
此规则的唯一例外是将指针传递给全局和静态存储变量. Swift确保您获得的指针值对于该特定变量是稳定且唯一的.来自Swift团队的blog post on Interacting with C Pointers(强调我的):
However,interaction with C pointers is inherently
unsafe compared to your other Swift code,so care must be taken. In
particular:
- These conversions cannot safely be used if the callee
saves the pointer value for use after it returns. The pointer that
results from these conversions is only guaranteed to be valid for the
duration of a call. Even if you pass the same variable,array,or
string as multiple pointer arguments,you could receive a different
pointer each time. An exception to this is global or static stored
variables. You can safely use the address of a global variable as a
persistent unique pointer value,e.g.: as a KVO context parameter.
因此,如果您将密钥设置为A的静态存储属性或仅仅是全局存储变量,则可以保证在两个函数调用中获得相同的指针值.
更改功能签名
When I change the function signature of
bbb
to make it the same withaaa
,the result of two prints are the same
这似乎是一个优化的事情,因为我只能在-O版本和游乐场中重现它.在未优化的构建中,添加或删除额外参数无效.
(虽然值得注意的是你不应该在游乐场中测试Swift行为,因为它们不是真正的Swift环境,并且可以对使用swiftc编译的代码展示不同的运行时行为)
这种行为的原因仅仅是巧合 – 第二个临时变量能够驻留在与第一个临时变量相同的地址(在第一个被解除分配之后).当您向aaa添加额外参数时,将在它们之间分配一个新变量以保存要传递的参数值,从而阻止它们共享相同的地址.
由于a的中间负载以便为a.key的值调用getter,因此在未优化的构建中不能观察到相同的地址.作为优化,如果编译器具有带有常量表达式的属性初始化器,则编译器能够将a.key的值内联到调用站点,从而无需使用此中间负载.
因此,如果给a.key一个非确定的值,例如var key = arc4random(),那么你应该再次观察不同的指针值,因为a.key的值不能再内联.
但无论原因如何,这都是如何不依赖变量(不是全局或静态存储变量)的指针值的完美示例 – 因为您获得的值可以根据优化级别等因素完全改变和参数计数.
inout& UnsafeMutable(RAW)指针
关于your comment:
But since
withUnsafePointer(to:_:)
always has the correct behavior I want (in fact it should,otherwise this function is of no use),and it also has aninout
parameter. So I assume there are implementation difference between these functions withinout
parameters.
编译器以与UnsafeRawPointer参数略有不同的方式处理inout参数.这是因为你可以在函数调用中改变inout参数的值,但是你不能改变UnsafeRawPointer的指针.
为了使调用者看到的inout参数值的任何突变,编译器通常有两个选项:
>将临时变量初始化为变量getter返回的值.使用指向此变量的指针调用该函数,并在函数返回后,使用临时变量的(可能已突变的)值调用变量的setter.
>如果它是可寻址的,只需使用指向变量的直接指针调用该函数.
如上所述,编译器不能将第二个选项用于未知最终的存储属性(但这可以随着优化而改变).但是,对于大值,始终依赖第一个选项可能会很昂贵,因为它们必须被复制.这对于具有写时复制行为的值类型尤其有害,因为它们依赖于唯一性以便对其底层缓冲区执行直接突变 – 临时副本违反了这一点.
为了解决这个问题,Swift实现了一个特殊的访问器 – 名为materializeForSet
.这个访问器允许被调用者为调用者提供指向给定变量的直接指针(如果它是可寻址的),否则将返回指向包含副本的临时缓冲区的指针.变量,在使用后需要写回setter.
前者是你在inout – you’re getting a direct pointer中看到的从materializeForSet返回a.key的行为,因此你在两个函数调用中得到的指针值是相同的.
但是,materializeForSet仅用于需要回写的函数参数,这解释了为什么它不用于UnsafeRawPointer.如果你使用aaa和bbb的函数参数采用UnsafeMutable(Raw)指针(需要回写),你应该再次观察相同的指针值.
func aaa(_ key: UnsafeMutableRawPointer) { print(key) } func bbb(_ key: UnsafeMutableRawPointer) { print(key) } class A { var key = "aaa" } var a = A() // will use materializeForSet to get a direct pointer to a.key aaa(&a.key) // 0x0000000100b00580 bbb(&a.key) // 0x0000000100b00580
但同样,如上所述,对于非全局或静态的变量,不应依赖此行为.