当 Swift 中的函数签名不同时,为什么 UnsafeRawPointer 显示不同的结果? [英] Why UnsafeRawPointer shows different result when function signatures differs in Swift?

查看:35
本文介绍了当 Swift 中的函数签名不同时,为什么 UnsafeRawPointer 显示不同的结果?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

以下代码可以在 Swift Playground 中运行:

导入 UIKitfunc aaa(_ key: UnsafeRawPointer!, _ value: Any! = nil) {打印(键)}func bbb(_ key: UnsafeRawPointer!) {打印(键)}A类{var key = "aaa"}让 a = A()aaa(&a.key)bbb(&a.key)

这是打印在我的 mac 上的结果:

0x00007fff5dce92480x00007fff5dce9220

为什么两次打印的结果不同?更有意思的是,当我把bbb的函数签名改成与aaa相同时,两次打印的结果是一样的.如果我在这两个函数调用中使用 global var 而不是 a.key ,则两次打印的结果是相同的.有谁知道为什么会发生这种奇怪的行为?

解决方案

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

因为对于每个函数调用,Swift 正在创建一个临时变量,初始化为 a.key 的 getter 返回的值.每个函数都使用一个指向他们给定临时变量的指针来调用.因此,指针值可能会不一样——因为它们指的是不同变量.

这里使用临时变量的原因是因为A是一个非final类,因此可以将其key的getter和setter覆盖 通过子类(可以很好地将其重新实现为计算属性).

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

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

class A {最后的 var 键 = "aaa"}var a = A()aaa(&a.key)//0x0000000100a0abe0bbb(&a.key)//0x0000000100a0abe0

因为现在key的地址可以只是直接传递给函数,完全绕过其getter.

但是值得注意的是,一般来说,您不应该依赖这种行为.您在函数中获得的指针的值是一个纯粹的实现细节,保证是稳定的.编译器可以随意调用函数,只向你保证你得到的指针在调用期间是有效的,并且将指针初始化为预期值(如果是可变的,你对调用者会看到指针).

此规则的唯一例外是传递指向全局和静态存储变量的指针.Swift 确实保证您获得的指针值对于该特定变量将是稳定且唯一的.来自 Swift 团队的关于与 C 指针交互的博文(重点是我的):

<块引用>

然而,与 C 指针的交互本质上是与您的其他 Swift 代码相比不安全,因此必须小心.在特别:

  • 如果被调用方无法安全地使用这些转换保存指针值以供返回后使用.那个指针这些转换的结果只保证对通话时长.即使您传递相同的变量、数组或字符串作为多个指针参数,您可能会收到不同的每次都是指针.一个例外是全局或静态存储变量.您可以安全地使用全局变量的地址作为持久的唯一指针值,例如:作为 KVO 上下文参数.

因此,如果您将 key 设为 A 的静态存储属性或只是一个全局存储变量,则可以保证在两个函数调用中获得相同的指针值.

<小时>

更改函数签名

<块引用>

当我把bbb的函数签名改成和aaa一样时,两次打印的结果是一样的

这似乎是一个优化问题,因为我只能在 -O 版本和 Playgrounds 中重现它.在未优化的构建中,添加或删除额外参数不起作用.

(尽管值得注意的是,您不应该在 Playgrounds 中测试 Swift 行为,因为它们不是真正的 Swift 环境,并且可以表现出与使用 swiftc 编译的代码不同的运行时行为)

这种行为的原因仅仅是一个巧合——第二个临时变量能够与第一个(在第一个被释放之后)驻留在相同地址.当您向 aaa 添加额外参数时,将在它们之间分配一个新变量以保存要传递的参数的值,从而防止它们共享相同的地址.

由于 a 的中间负载,在未优化的构建中无法观察到相同的地址,以便为 a.key 的值调用 getter.作为优化,编译器能够将 a.key 的值内联到调用站点,如果它有一个带有常量表达式的属性初始化器,则不需要这种中间负载.

因此,如果您给 a.key 一个不确定的值,例如 var key = arc4random(),那么您应该再次观察不同的指针值,因为a.key 的值不能再内联.

但不管原因如何,这是一个完美示例,说明如何依赖变量(不是全局或静态存储变量)的指针值on – 因为您获得的值可以根据优化级别和参数数量等因素完全改变.

<小时>

inout &UnsafeMutable(Raw)Pointer

关于您的评论:

<块引用>

但是由于 withUnsafePointer(to:_:) 总是有我想要的正确行为(实际上它应该,否则这个函数没有用),而且它还有一个 inout 参数.所以我假设这些带有 inout 参数的函数之间存在实现差异.

编译器以与 UnsafeRawPointer 参数稍微不同的方式处理 inout 参数.这是因为您可以在函数调用中改变 <​​code>inout 参数的值,但不能改变 UnsafeRawPointerpointee.>

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

  1. 将临时变量初始化为变量的 getter 返回的值.使用指向此变量的指针调用函数,一旦函数返回,就使用临时变量的(可能已更改的)值调用变量的 setter.

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

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

为了解决这个问题,Swift 实现了一个特殊的访问器——叫做 materializeForSet.这个访问器允许被调用者向调用者提供一个直接指向给定变量的指针(如果它是可寻址的),或者将返回一个指向包含变量副本的临时缓冲区的指针,这将需要使用后写回 setter.

前者是您在 inout 中看到的行为 – 你从 materializeForSet 得到一个直接指针a.key,因此,您在两个函数调用中获得的指针值是相同的.

然而,materializeForSet 只用于需要回写的函数参数,这就解释了为什么它不用于UnsafeRawPointer.如果你让aaabbb的函数参数取UnsafeMutable(Raw)Pointers(do需要写-back),您应该再次观察相同的指针值.

func aaa(_ key: UnsafeMutableRawPointer) {打印(键)}func bbb(_ 键: UnsafeMutableRawPointer) {打印(键)}A类{var key = "aaa"}var a = A()//将使用 materializeForSet 获取指向 a.key 的直接指针aaa(&a.key)//0x0000000100b00580bbb(&a.key)//0x0000000100b00580

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

The code below can be run in a 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)

Here's the result printed on my mac:

0x00007fff5dce9248
0x00007fff5dce9220

Why the results of two prints differs? What's more interesting, when I change the function signature of bbb to make it the same with aaa, the result of two prints are the same. And if I use a global var instead of a.key in these two function calls, the result of two prints are the same. Does anyone knows why this strange behavior happens?

解决方案

Why the results of two prints differs?

Because for each function call, Swift is creating a temporary variable initialised to the value returned by a.key's getter. Each function is called with a pointer to their given temporary variable. Therefore the pointer values will likely not be the same – as they refer to different variables.

The reason why temporary variables are used here is because A is a non-final class, and can therefore have its getters and setters of key overridden by subclasses (which could well re-implement it as a computed property).

Therefore in an un-optimised build, the compiler cannot just pass the address of key directly to the function, but instead has to rely on calling the getter (although in an optimised build, this behaviour can change completely).

You'll note that if you mark key as final, you should now get consistent pointer values in both functions:

class A {
    final var key = "aaa"
}

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

Because now the address of key can just be directly passed to the functions, bypassing its getter entirely.

It's worth noting however that, in general, you should not rely on this behaviour. The values of the pointers you get within the functions are a pure implementation detail and are not guaranteed to be stable. The compiler is free to call the functions however it wishes, only promising you that the pointers you get will be valid for the duration of the call, and will have pointees initialised to the expected values (and if mutable, any changes you make to the pointees will be seen by the caller).

The only exception to this rule is the passing of pointers to global and static stored variables. Swift does guarantee that the pointer values you get will be stable and unique for that particular variable. From the Swift team's blog post on Interacting with C Pointers (emphasis mine):

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.

Therefore if you made key a static stored property of A or just a global stored variable, you are guaranteed to the get same pointer value in both function calls.


Changing the function signature

When I change the function signature of bbb to make it the same with aaa, the result of two prints are the same

This appears to be an optimisation thing, as I can only reproduce it in -O builds and playgrounds. In an un-optimised build, the addition or removal of an extra parameter has no effect.

(Although it's worth noting that you should not test Swift behaviour in playgrounds as they are not real Swift environments, and can exhibit different runtime behaviour to code compiled with swiftc)

The cause of this behaviour is merely a coincidence – the second temporary variable is able to reside at the same address as the first (after the first is deallocated). When you add an extra parameter to aaa, a new variable will be allocated 'between' them to hold the value of the parameter to pass, preventing them from sharing the same address.

The same address isn't observable in un-optimised builds due to the intermediate load of a in order to call the getter for the value of a.key. As an optimisation, the compiler is able to inline the value of a.key to the call-site if it has a property initialiser with a constant expression, removing the need for this intermediate load.

Therefore if you give a.key a non-determininstic value, e.g var key = arc4random(), then you should once again observe different pointer values, as the value of a.key can no longer be inlined.

But regardless of the cause, this is a perfect example of how the pointer values for variables (which are not global or static stored variables) are not to be relied on – as the value you get can completely change depending on factors such as optimisation level and parameter count.


inout & UnsafeMutable(Raw)Pointer

Regarding 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.

The compiler treats an inout parameter in a slightly different way to an UnsafeRawPointer parameter. This is because you can mutate the value of an inout argument in the function call, but you cannot mutate the pointee of an UnsafeRawPointer.

In order to make any mutations to the value of the inout argument visible to the caller, the compiler generally has two options:

  1. Make a temporary variable initialised to the value returned by the variable's getter. Call the function with a pointer to this variable, and once the function has returned, call the variable's setter with the (possibly mutated) value of the temporary variable.

  2. If it's addressable, simply call the function with a direct pointer to the variable.

As said above, the compiler cannot use the second option for stored properties that aren't known to be final (but this can change with optimisation). However, always relying on the first option can be potentially expensive for large values, as they'll have to be copied. This is especially detrimental for value types with copy-on-write behaviour, as they depend on being unique in order to perform direct mutations to their underlying buffer – a temporary copy violates this.

To solve this problem, Swift implements a special accessor – called materializeForSet. This accessor allows the callee to either provide the caller with a direct pointer to the given variable if it's addressable, or otherwise will return a pointer to a temporary buffer containing a copy of the variable, which will need to be written back to the setter after it has been used.

The former is the behaviour you're seeing with inoutyou're getting a direct pointer to a.key back from materializeForSet, therefore the pointer values you get in both function calls are the same.

However, materializeForSet is only used for function parameters that require write-back, which explains why it's not used for UnsafeRawPointer. If you make the function parameters of aaa and bbb take UnsafeMutable(Raw)Pointers (which do require write-back), you should observe the same pointer values again.

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

But again, as said above, this behaviour is not to be relied upon for variables that are not global or static.

这篇关于当 Swift 中的函数签名不同时,为什么 UnsafeRawPointer 显示不同的结果?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆