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

时间:2017-03-16 09:25:01

标签: 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 相同时,两次打印的结果是相同的。如果我在这两个函数调用中使用全局变量而不是 a.key ,则两次打印的结果是相同的。有谁知道为什么会发生这种奇怪的行为?

1 个答案:

答案 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参数值的任何突变,编译器通常有两个选项:

  1. 将临时变量初始化为变量“getter”返回的值。使用指向此变量的指针调用该函数,并在函数返回后,使用临时变量的(可能已突变的)值调用变量的setter。

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

  3. 如上所述,编译器不能将第二个选项用于不知道为inout的存储属性(但这可以随着优化而改变)。但是,对于大值,始终依赖第一个选项可能会非常昂贵,因为它们必须被复制。对于具有写时复制行为的值类型,这尤其是有害的,因为它们依赖于唯一的,以便对其底层缓冲区执行直接变换 - 临时副本违反了这一点。

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

    前者是您从final返回inout - you're getting a direct pointera.key的行为,因此您在两个函数调用中都会获得指针值是一样的。

    但是,materializeForSet仅用于需要回写的函数参数,这解释了为什么它不用于materializeForSet。如果您将UnsafeRawPointeraaa的函数参数设为bbb s(其中执行需要回写),则应再次观察相同的指针值。

    UnsafeMutable(Raw)Pointer

    但是,如上所述,对于非全局或静态的变量,此行为依赖。