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

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

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 with aaa,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 an inout parameter. So I assume there are implementation difference between these functions with inout 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

但同样,如上所述,对于非全局或静态的变量,不应依赖此行为.

相关文章

软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘...
现实生活中,我们听到的声音都是时间连续的,我们称为这种信...
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿...
【Android App】实战项目之仿抖音的短视频分享App(附源码和...
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至...
因为我既对接过session、cookie,也对接过JWT,今年因为工作...