为什么 Swift 的 UnsafePointer 和 UnsafeBufferPointer 不可互换?



我一直在使用苹果的神经网络工具,这意味着我一直在用不安全的指针。我和C一起长大,我和Swift一起工作了很长一段时间,所以我很乐意使用它们,但有一点让我完全不知所措。

我不明白为什么要从另一种指针中派生出一种不安全的指针。一般来说,这看起来应该是微不足道的,但不同类型的初始化器对它们将接受的输入类型是特定的,我很难弄清楚规则。

一个简单而具体的例子,也许是最让我头疼的

// The neural net components want mutable raw memory, and it's easier
// to build them up from the bottom, so: raw memory
let floats = 100
let bytes = floats * MemoryLayout<Float>.size
let raw = UnsafeMutableRawPointer.allocate(byteCount: bytes, alignment: MemoryLayout<Float>.alignment)
// Higher up in the app, I want to use memory that just looks like an array
// of Floats, to minimize the ugly unsafe stuff everywhere. So I'll use
// a buffer pointer, and that's where the first confusing thing shows up:
// This won't work
// let inputs = UnsafeMutableBufferPointer<Float>(start: raw, count: floats)
// The initializer won't take raw memory. But it will take a plain old
// UnsafePointer<Float>. Where to get that? you can get it from the raw pointer
let unsafeMutablePointer = raw.bindMemory(to: Float.self, capacity: floats)
// Buf if that's possible, then why wouldn't there be a buffer pointer initializer for it?
// Of course, with the plain old pointer, I can get my buffer pointer
let inputs = UnsafeMutableBufferPointer(start: unsafeMutablePointer, count: floats)

虽然我还没有找到任何关于不同种类背后的理论的讨论,但我在本教程中确实找到了线索。有一张图表比较了不同的类型,上面说普通的旧UnsafePointer是可跨的,但不是集合,而UnsafeBufferPointer是集合,但不可跨。

我理解一个集合的概念,它不可跨越,就像一个集合。但这两种类型的不安全指针允许使用下标。它们就像普通数组一样工作,在我看来,它们都是可跨越的集合。也许其中有一条微妙的线索,我错过了。

为什么不能从一个可以获得UnsafePointer的类型中获得UnsafeBufferPointer

这些类型之间的区别并不像你想象的那么大。从概念上讲,UnsafeBufferPointer可以被视为(UnsafePointer, Int)的元组,即指向内存中具有已知计数的元素缓冲区的指针。相反,UnsafePointer是指向内存中计数为未知的元素的指针;UnsafePointer更接近地代表了你在C中可能习惯的任意指针:它可能指向单个元素,或者指向几个元素的连续分组的开始,但就其本身而言,没有办法找到。

具有已知计数的UnsafeBufferPointer也意味着它能够符合Collection(需要已知的开始和结束),而不是不具有该信息的UnsafePointer

Swift在很大程度上是一种语义语言,它非常强调在类型系统中表达有关可用工具的知识。正如您所指出的,您可以对一种类型执行某些操作,而不能对另一种类型进行操作——这是出于设计目的,使某些操作更难错误执行。

这些指针也是可转换的:

  • UnsafeBufferPointer有一个baseAddressUnsafePointer:给定一个缓冲区,你总是可以"扔掉";有关获取基础未计数指针的计数的信息
  • 给定UnsafePointercount,也可以用UnsafeBufferPointer.init(start:count:)来表示内存中是否存在缓冲区

一般的答案是:使用最具体的指针类型来表示您所拥有的数据。如果您指向多个元素,并且知道您有多少元素,那么通常最好使用Buffer变体的指针。类似地,如果您指向内存中的任意字节(可能有类型,也可能没有类型),那么如果可能的话,您应该使用Raw指针。(当然,如果你需要写入内存中的这些位置,你也需要使用这些位置的Mutable变体。)

有关更多信息,我强烈推荐Andrew Trick在WWDC 2020中关于这个主题的演讲:在Swift中安全管理指针。他详细介绍了Swift中表示指针寿命的概念状态机,以及如何在指针类型之间转换和正确使用指针类型。(当你谈论这个话题时,它尽可能地靠近马的嘴。)


另外,关于您的示例代码:@Sweeper在评论中正确地指出,如果您希望分配Floats的缓冲区,则不应该分配原始缓冲区并绑定其内存类型。一般来说,分配原始缓冲区不仅存在错误确定所需缓冲区大小的风险,而且还存在不考虑填充(对于某些类型,必须手动计算填充)的风险。

相反,应该使用UnsafeMutableBufferPointer.allocate(capacity:)来分配缓冲区,然后可以对其进行写入。它正确地考虑了对齐和填充,所以不会出错。

原始记忆和类型记忆之间的区别在Swift中非常微妙,Andy在关联对话中描述得比我在这里描述的要好得多,但tl;dr:原始内存是一个非类型化字节的集合,可以表示任何东西,而类型化内存只表示特定类型的值(除了少数例外,不能安全地任意重新解释,这与C有很大不同!);您几乎应该永远不必手动绑定内存,如果您将内存绑定到非平凡类型,那么您几乎肯定做错了。(不是说你在这里这么做,只是提醒一下)


最后,关于StrideableCollection的主题,以及下标——你可以在两者中下标的事实与C的行为相匹配,但在Swift中有一个微妙的语义区别。

订阅UnsafePointer在很大程度上意味着它在C中的作用:UnsafePointer知道它的基类型,并且引用内存中的单个位置,可以使用类型的对齐和填充来计算内存中该类型的下一个对象的位置(这就是它的Strideable一致性所暗示的);下标允许您访问内存中相对于指针所指对象的几个连续对象中的一个。此外,就像在C中一样:因为您不知道一组这样的对象的结尾在哪里,所以您可以使用UnsafePointer进行无边界检查的任意下标——根本无法提前知道您试图进行的访问是否有效。

另一方面,通过UnsafeBufferPointer进行下标就像访问内存中元素集合内的元素。因为缓冲区的起点和终点都有明确的边界,所以可以进行边界检查,在UnsafeBufferPointer上进行越界索引显然是一个错误。沿着这些思路,UnsafeBufferPointer上的Strideable一致性没有多大意义:;"步幅";CCD_ 33类型表示它知道如何到达";下一个";一个,但没有一个合乎逻辑的";下一个";在整个CCD_ 34之后缓冲。

因此,这两种类型最终都有一个下标运算符,有效地执行相同的操作,但在语义上有着截然不同的含义。

相关内容

  • 没有找到相关文章

最新更新