以下代码可以在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相同时 ,两次打印的结果相同。如果在这两个函数调用中使用 全局var 而不是 a.key ,则两次打印的结果是相同的。有谁知道为什么会发生这种奇怪的行为?
为什么两次打印的结果不同?
因为对于每个函数调用,Swift都会创建一个临时变量,该临时变量初始化为a.key的getter 返回的值。每个函数都使用指向 其 给定临时变量的指针进行调用。因此,指针值可能会不同,因为它们引用了 不同的 变量。
a.key
之所以在这里使用临时变量,是因为A它是非最终类,因此可以使其子类的getter和setter方法key 覆盖 子类(可以很好地将其实现为计算属性)。
A
key
因此,在未优化的构建中,编译器不能只是key直接将的地址传递给函数,而必须依赖于调用getter(尽管在优化的构建中,此行为可以完全改变)。
您会注意到,如果将其标记key为final,您现在应该在两个函数中获得一致的指针值:
final
class A { final var key = "aaa" } var a = A() aaa(&a.key) // 0x0000000100a0abe0 bbb(&a.key) // 0x0000000100a0abe0
因为现在的地址key 可以 只被直接传递给函数,完全绕过其吸气剂。
但是,值得注意的是,通常来说,您 不应该 依赖此行为。在函数中获得的指针的值是纯实现细节,并且 不能 保证稳定。编译器可以随心所欲地调用函数,只是向您保证,所获得的指针将在调用期间有效,并且将指针初始化为期望值(如果可变,则对指针进行的任何更改)。呼叫者将看到被指示者)。
该规则的 唯一 例外是传递指向全局和静态存储变量的指针。Swift 确实 确保您获得的指针值对于该特定变量将是稳定且唯一的。从Swift团队关于与C指针交互的博客文章中(重点是我的):
但是,与其他Swift代码相比,与C指针进行交互本质上是不安全的,因此必须格外小心。特别是: * 如果被调用方在返回后保存指针值以供使用,则不能安全地使用这些转换。这些转换产生的指针仅在呼叫期间有效。即使您将相同的变量,数组或字符串作为多个指针参数传递,也可能每次都收到不同的指针。 全局或静态存储的变量是一个例外。 您可以安全地将全局变量的地址用作持久唯一指针值,例如:作为KVO上下文参数。
但是,与其他Swift代码相比,与C指针进行交互本质上是不安全的,因此必须格外小心。特别是:
* 如果被调用方在返回后保存指针值以供使用,则不能安全地使用这些转换。这些转换产生的指针仅在呼叫期间有效。即使您将相同的变量,数组或字符串作为多个指针参数传递,也可能每次都收到不同的指针。 全局或静态存储的变量是一个例外。 您可以安全地将全局变量的地址用作持久唯一指针值,例如:作为KVO上下文参数。
因此,如果您将key静态存储属性A设为或只是将全局存储变量设为全局变量,则可以确保在两个函数调用中获得相同的指针值。
当我更改的功能bbb使其与相同时aaa,两次打印的结果相同
bbb
aaa
这似乎是一项优化工作,因为我只能在-O建筑物和游乐场中进行复制。在未优化的版本中,添加或删除额外的参数无效。
(尽管值得注意的是,您不应在运动场中测试Swift行为,因为它们不是真正的Swift环境,并且可能会与使用编译的代码表现出不同的运行时行为swiftc)
swiftc
此行为的原因仅仅是一个巧合-第二个临时变量能够与第一个临时变量驻留在 相同的 地址(在第一个临时变量被释放之后)。当您在中添加额外的参数时aaa,将在它们之间“分配”新变量以保存要传递的参数值,从而防止它们共享相同的地址。
在未优化的版本中,由于a要调用getter以获取值的中间负载,因此无法观察到相同的地址a.key。作为优化,如果编译器a.key具有带有常量表达式的属性初始化程序,则它可以将其值内联到调用站点,从而消除了对此中间负载的需要。
a
因此,如果您提供a.key一个不确定的值,例如var key = arc4random(),那么您应该再次观察不同的指针值,因为a.key不能再内联的值。
var key = arc4random()
但是,无论原因如何,这都是一个 很好的 示例,说明如何 不 依赖变量(不是全局变量或静态存储的变量)的指针值- 因为您获得的值可以根据优化级别等因素而完全改变和参数计数。
inout
UnsafeMutable(Raw)Pointer
但是由于withUnsafePointer(to:_:)始终具有我想要的正确行为(实际上应该如此,否则此功能没有用),并且它还具有一个inout参数。因此,我假设这些函数与inout参数之间在实现上存在差异。
withUnsafePointer(to:_:)
编译器对待inout参数的方式与参数 略有 不同UnsafeRawPointer。这是因为你可以变异的价值inout在函数调用的参数,但你不能在发生变异pointee的UnsafeRawPointer。
UnsafeRawPointer
pointee
为了使inout参数值的任何变化对调用者可见,编译器通常具有两个选项:
将一个临时变量初始化为该变量的getter返回的值。使用指向该变量的指针来调用该函数,一旦函数返回,请使用临时变量的(可能是变异的)值来调用变量的setter。
如果它是可寻址的,则只需使用 直接 指向该变量的指针来调用该函数。
如上所述,编译器无法对未知的存储属性使用第二个选项final(但这可以随着优化而改变)。但是,对于大值而言,始终依赖第一个选项可能会非常昂贵,因为必须将其复制。这对于具有写时复制行为的值类型 特别 有害,因为它们依赖于唯一性才能对其基础缓冲区执行直接突变-临时副本违反了这一点。
为了解决这个问题,Swift实现了一个特殊的访问器– materializeForSet。该访问器允许被调用方为调用方提供指向给定变量的 直接 指针(如果可寻址的话),否则将返回指向包含该变量副本的临时缓冲区的指针,此缓冲区需要在写完后写回给setter它已被使用。
materializeForSet
前者是你与看到的行为inout- 你得到一个 直接 指针来a.key从后面materializeForSet,所以你在这两个函数调用得到的指针值是相同的。
但是,materializeForSet仅用于需要回写的功能参数,这说明了为什么不将其用于的原因UnsafeRawPointer。如果将函数参数设置为aaa并bbb取UnsafeMutable(Raw)Pointers( 确实 需要回写),则应再次观察相同的指针值。
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
但同样,就像上面说,这种行为是 不是 在为不属于全局或静态变量的依据。