一尘不染

当Swift中的函数签名不同时,为什么UnsafeRawPointer会显示不同的结果?

swift

以下代码可以在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 ,则两次打印的结果是相同的。有谁知道为什么会发生这种奇怪的行为?


阅读 218

收藏
2020-07-07

共1个答案

一尘不染

为什么两次打印的结果不同?

因为对于每个函数调用,Swift都会创建一个临时变量,该临时变量初始化为a.key的getter
返回的值。每个函数都使用指向 给定临时变量的指针进行调用。因此,指针值可能会不同,因为它们引用了 不同的 变量。

之所以在这里使用临时变量,是因为A它是非最终类,因此可以使其子类的getter和setter方法key 覆盖
子类(可以很好地将其实现为计算属性)。

因此,在未优化的构建中,编译器不能只是key直接将的地址传递给函数,而必须依赖于调用getter(尽管在优化的构建中,此行为可以完全改变)。

您会注意到,如果将其标记keyfinal,您现在应该在两个函数中获得一致的指针值:

class A {
    final var key = "aaa"
}

var a = A()
aaa(&a.key) // 0x0000000100a0abe0
bbb(&a.key) // 0x0000000100a0abe0

因为现在的地址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不能再内联的值。

但是,无论原因如何,这都是一个 很好的 示例,说明如何 依赖变量(不是全局变量或静态存储的变量)的指针值-
因为您获得的值可以根据优化级别等因素而完全改变和参数计数。


inoutUnsafeMutable(Raw)Pointer

但是由于withUnsafePointer(to:_:)始终具有我想要的正确行为(实际上应该如此,否则此功能没有用),并且它还具有一个inout参数。因此,我假设这些函数与inout参数之间在实现上存在差异。

编译器对待inout参数的方式与参数 略有
不同UnsafeRawPointer。这是因为你可以变异的价值inout在函数调用的参数,但你不能在发生变异pointeeUnsafeRawPointer

为了使inout参数值的任何变化对调用者可见,编译器通常具有两个选项:

  1. 将一个临时变量初始化为该变量的getter返回的值。使用指向该变量的指针来调用该函数,一旦函数返回,请使用临时变量的(可能是变异的)值来调用变量的setter。

  2. 如果它是可寻址的,则只需使用 直接 指向该变量的指针来调用该函数。

如上所述,编译器无法对未知的存储属性使用第二个选项final(但这可以随着优化而改变)。但是,对于大值而言,始终依赖第一个选项可能会非常昂贵,因为必须将其复制。这对于具有写时复制行为的值类型
特别 有害,因为它们依赖于唯一性才能对其基础缓冲区执行直接突变-临时副本违反了这一点。

为了解决这个问题,Swift实现了一个特殊的访问器–
materializeForSet。该访问器允许被调用方为调用方提供指向给定变量的 直接
指针(如果可寻址的话),否则将返回指向包含该变量副本的临时缓冲区的指针,此缓冲区需要在写完后写回给setter它已被使用。

前者是你与看到的行为inout- 你得到一个 直接
指针
a.key从后面materializeForSet,所以你在这两个函数调用得到的指针值是相同的。

但是,materializeForSet仅用于需要回写的功能参数,这说明了为什么不将其用于的原因UnsafeRawPointer。如果将函数参数设置为aaabbbUnsafeMutable(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

但同样,就像上面说,这种行为是 不是 在为不属于全局或静态变量的依据。

2020-07-07