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

Why are Swift's UnsafePointer and UnsafeBufferPointer not interchangeable?

我一直在使用 Apple 的神经网络工具,这意味着我一直在使用不安全的指针。我是用 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)

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

我理解不可跨越的集合的概念,例如集合。但是这两类不安全指针是允许下标的。它们就像常规数组一样工作,在我看来它们都是可跨步的集合。也许那里有我遗漏的微妙线索。

为什么无法从可以从中获取 UnsafePointer 的类型中获取 UnsafeBufferPointer

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

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

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

这些指针也是可以转换的:

  • UnsafeBufferPointer 有一个 baseAddress,其中 一个 UnsafePointer:给定一个缓冲区,您总是可以“丢弃”有关count 获取底层未计数指针
  • 给定一个UnsafePointer和一个count,你还可以用UnsafeBufferPointer.init(start:count:)
  • 表示内存中存在缓冲区

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

有关更多信息,我强烈推荐 Andrew Trick 在 WWDC 2020 上关于此主题的演讲:Safely manage pointers in Swift。他详细介绍了 Swift 中表示指针生命周期的概念状态机,以及如何在指针类型之间进行转换和正确使用。 (说到这个话题就离马嘴最近了。)


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

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

原始内存和类型化内存之间的区别在 Swift 中非常微妙,Andy 在链接演讲中对它的描述比我在这里描述的要好得多,但是 tl;dr:原始内存是非类型化内存的集合可以代表任何东西的字节,而类型化内存代表 特定类型的值(并且不能安全地任意重新解释,除了一些例外,这是与 C 的重大背离!);你应该几乎永远不会必须手动绑定内存,如果你将内存绑定到非平凡的类型,你几乎肯定做错了。 (并不是说你在这里这样做,只是提醒一下)


最后,关于 StrideableCollection 和下标的主题 — 您可以为两者下标的事实与 C 的行为相匹配,但有一个微妙的 Swift.

中的语义 区别

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

另一方面,通过 UnsafeBufferPointer 下标就像访问内存中元素集合 inside 中的一个元素。因为缓冲区的开始和结束位置有明确的界限,所以您可以进行边界检查,并且 UnsafeBufferPointer 上的索引越界显然是一个错误。按照这些思路,StrideableUnsafeBufferPointer 的一致性没有多大意义:Strideable 类型的“步幅”表明它知道如何到达“下一个”类型,但是在整个 UnsafeBufferPointer.

之后没有逻辑上的“下一个”缓冲区

所以这两种类型都以下标运算符结尾,有效地执行相同的操作,但在语义上具有非常不同的含义。