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