微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决为什么 Swift 的 UnsafePointer 和 UnsafeBufferPointer 不能互换?

我一直在使用 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一个集合但不是可跨步的。

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

为什么不能从可以从中获取 UnsafeBufferPointer 的类型中获取 UnsafePointer

解决方法

这些类型之间的区别并不像您想象的那么大。从概念上讲,UnsafeBufferPointer 可以被视为 (UnsafePointer,Int) 的元组,即指向内存中具有已知计数的元素缓冲区的指针。相比之下,UnsafePointer 是指向内存中一个未知计数的元素的指针; 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 在评论中正确指出,如果您要分配 Float 的缓冲区,则不应分配原始缓冲区并绑定其内存类型。一般而言,分配原始缓冲区不仅存在误认为所需缓冲区大小的风险,而且还存在未考虑填充(对于某些类型必须手动计算)的风险。

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

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


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

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

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

所以这两种类型最终都带有一个下标运算符,它有效执行相同的操作,但语义上却有着截然不同的含义。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。