【问题标题】:Why are Swift's UnsafePointer and UnsafeBufferPointer not interchangeable?为什么 Swift 的 UnsafePointer 和 UnsafeBufferPointer 不能互换?
【发布时间】:2021-05-20 23:15:15
【问题描述】:

我一直在使用 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?

【问题讨论】:

  • "那为什么没有缓冲区指针初始化器呢?这是一个只有设计师才能告诉您原因的设计决策。无论如何,您可以从UnsafeMutablePointer&lt;Float&gt;.allocate 开始,而不是UnsafeMutableRawPointer.allocate,不是吗?
  • 另外,我有点担心你如何称呼bindMemory。 capacity 是您要绑定的 Float 的实例数,而不是字节数。所以你可能应该通过100 / MemoryLayout&lt;Float&gt;.size 来代替。
  • 另请注意,您可以将UnsafeMutableRawPointer 直接转为UnsafeRawBufferPointer。这里的设计似乎是,如果你有一个原始指针,你可以 1) 使它不是原始的,或者 2) 使它成为一个缓冲区,如果你想做这两件事,你需要两个步骤。
  • 很好地了解缓冲区的大小,非常感谢

标签: swift unsafe-pointers


【解决方案1】:

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

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

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

这些指针也可以转换:

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

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

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


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

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

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


最后,关于 Strideable 与 Collection 的主题,以及下标——事实上,您可以下标到两者的行为与 C 的行为相匹配,但在 Swift 中有细微的语义区别.

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

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

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

【讨论】:

  • 这很有帮助,谢谢。我很高兴你提到了Strideable 和UnsafeBufferPointer。这解释了很多事情,干杯
  • 我只需要告诉你,Itai,我已经多次回来重新阅读你的答案;你在 Swift 的一部分上发出了令人难以置信的明亮光芒,它总是看起来有点阴暗。干杯
  • @SaganRitual 非常感谢您的客气话。 :) 能帮助解开这样棘手的话题并尽我所能做到这一点真的很高兴。赞赏!
猜你喜欢
  • 2020-03-22
  • 1970-01-01
  • 2020-10-24
  • 1970-01-01
  • 2017-05-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多