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

7
我一直在使用苹果的神经网络工具,这意味着我已经相当多地使用了不安全指针。我从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是集合但不能步进。
我理解集合无法步进的概念,例如set。但这两种类型的不安全指针允许下标。它们像常规数组一样工作,这听起来就像它们都是可步进的集合。也许我漏掉了某个微妙的提示。
为什么不能从你可以从中获取UnsafePointer的类型得到UnsafeBufferPointer呢?

“那么为什么它没有缓冲指针初始化器呢?”这是一个只有设计者才能告诉你的设计决策。无论如何,你可以从UnsafeMutablePointer<Float>.allocate开始,而不是从UnsafeMutableRawPointer.allocate开始,对吧? - Sweeper
1
另外,我有点担心你如何调用bindMemorycapacity是您想要绑定的Float实例数,而不是字节数。因此,您应该传递100 / MemoryLayout<Float>.size - Sweeper
还要注意,您可以直接将UnsafeMutableRawPointer转换为UnsafeRawBufferPointer。这里的设计似乎是,如果您有一个原始指针,您可以选择1)使其不是原始的,或2)将其变成缓冲区。如果您想同时执行这两个操作,则需要两个步骤。 - Sweeper
缓冲区大小的抓取得很好,非常感谢。 - SaganRitual
1个回答

21
这些类型之间的区别并没有你想象的那么大。从概念上讲,UnsafeBufferPointer 可以被视为一个元组 (UnsafePointer, Int),即指向内存中具有已知数量元素的缓冲区的指针。相比之下,UnsafePointer 是指向内存中元素的一个指针,但其数量是未知的;UnsafePointer 更接近于 C 中任意指针的概念:它可以指向单个元素或一组连续多个元素的起始位置,但单独使用时无法找到这些信息。 UnsafeBufferPointer 具有已知数量的特性,这也意味着它能够符合 Collection (需要知道起点和终点)而 UnsafePointer 则没有该信息。
Swift非常注重语义,强调在类型系统中表达有关可用工具的知识。正如您指出的,您可以对某种类型执行操作,但不能对另一种类型执行 —— 这是设计之初的考虑,目的是使某些操作更难以不正确地执行。
这些指针也是可以相互转换的:
- UnsafeBufferPointer 有一个 baseAddress,它是一个 UnsafePointer:给定一个缓冲区,您总是可以“丢弃”有关计数的信息以获得底层未计数指针。
  • 通过使用 UnsafeBufferPointer.init(start:count:),你可以使用UnsafePointercount表达内存中缓冲区的存在。
  • 一般来说,使用最精确的指针类型来表示所拥有的数据。如果你指向的是多个元素并且知道它们的数量,通常首选使用Buffer变量的指针。同样,如果你指向内存中的任意字节(可能有类型或没有类型),则应尽可能使用Raw指针。(当然,如果需要在这些内存位置写入内容,则还需要使用相应的Mutable变量的指针)。

    更多信息,请参考 Andrew Trick 在 WWDC 2020 上关于此主题的演讲:Safely manage pointers in Swift。他详细介绍了 Swift 中代表指针生命周期的概念状态机,以及如何正确地转换和使用指针类型。(在此话题上,这是尽可能接近权威性的来源。)


    关于你的示例代码:@Sweeper在评论中正确指出,如果你想分配一个 Float 缓冲区,你不应该分配一个原始缓冲区并绑定其内存类型。通常,分配原始缓冲区不仅有误计算所需缓冲区大小的风险,还可能没有考虑填充(某些类型必须手动计算填充)。

    相反,你应该使用 UnsafeMutableBufferPointer.allocate(capacity:) 来分配缓冲区,然后可以向其中写入数据。它能够正确考虑对齐和填充,因此你不会出错。

    在 Swift 中,原始内存和类型化内存之间的差异非常微妙,Andy 在链接的演讲中描述得比我更好,但是简而言之:原始内存是未类型化字节的集合,可以表示任何内容,而类型化内存仅表示特定类型的值(不能安全地重新解释为任意类型,除了几个例外,这与 C 相距甚远!);你几乎永远不需要手动绑定内存,如果将内存绑定到非平凡类型上,则几乎可以肯定这样做是错误的。(并不是说你在这里这么做了,只是提个醒)


    最后,关于StrideableCollection,以及下标操作——事实上,你可以在两者中都使用下标操作符,这与C语言的行为相同,但在Swift中有微妙的语义区别。

    UnsafePointer中进行下标操作,在很大程度上与C语言中的操作类似:一个UnsafePointer知道其基本类型,并且可以使用该类型的对齐方式和填充量来计算在内存中下一个对象的位置(这是它的Strideable一致性的含义);下标操作允许您访问相对于指针所引用的对象的连续几个对象中的其中一个。此外,就像在C语言中一样:由于您不知道这些对象组在哪里结束,因此可以使用UnsafePointer进行任意下标操作而不进行边界检查——事先根本无法知道您尝试进行的访问是否有效。

    另一方面,通过UnsafeBufferPointer进行下标访问就像是访问存储在内存中元素的集合中的一个元素。因为缓冲区开始和结束位置都有明确的边界,所以你得到了边界检查,并且在UnsafeBufferPointer上越界索引会更清楚地表明出错。沿着这些线路,UnsafeBufferPointer上的Strideable一致性没有太多意义: Strideable类型的“stride”表明它知道如何到达“下一个”,但在整个UnsafeBufferPointer后面没有逻辑上的“下一个”缓冲区。 因此,这两种类型最终都会具有实际上执行相同操作的下标运算符,但语义上却具有非常不同的含义。

    2
    这非常有帮助,谢谢。我很高兴你提到了StrideableUnsafeBufferPointer。这解释了很多事情,干杯! - SaganRitual
    1
    我必须告诉你,Itai,我已经多次回来重新阅读你的答案;你为Swift的某个部分照亮了一盏非常明亮的灯。干杯! - SaganRitual
    1
    @SaganRitual 非常感谢你的赞美之词。 :) 能够帮助解决这样棘手的问题并尽力而为,真是一种乐趣。非常感激! - Itai Ferber

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接