swift - 没有调试模式 UnsafePointer withMemoryRebound 将给出错误值
问题描述
在这里,我试图将 5 个字节连接到单个 Integer 值中,但我遇到了UnsafePointer withMemoryRebound
方法问题。当我调试和检查日志时,它会给出正确的值。但是当我尝试不调试时,它会给出错误的值。(5 次错误值中有 4 次)。我对这个 API 感到困惑。我使用的方法是否正确?
情况1:
let data = [UInt8](rowData) // rowData is type of Data class
let totalKM_BitsArray = [data[8],data[7],data[6],data[5],data[4]]
self.totalKm = UnsafePointer(totalKM_BitsArray).withMemoryRebound(to:UInt64.self, capacity: 1) {$0.pointee}
案例2:
下面的代码适用于启用或禁用调试模式并给出正确的值。
let byte0 : UInt64 = UInt64(data[4])<<64
let byte1 : UInt64 = UInt64(data[5])<<32
let byte2 : UInt64 = UInt64(data[6])<<16
let byte3 : UInt64 = UInt64(data[7])<<8
let byte4 : UInt64 = UInt64(data[8])
self.totalKm = byte0 | byte1 | byte2 | byte3 | byte4
请建议我使用 UnsafePointer 的方式?为什么会出现这个问题?
附加信息:
let totalKm : UInt64
let data = [UInt8](rowData)
// 数据包含 [100, 200, 28, 155, 0, 0, 0, 26, 244, 0, 0, 0, 45, 69, 0, 0, 0, 4, 246]
let totalKM_BitsArray = [data[8],data[7],data[6],data[5],data[4]]
// 包含 [ 244,26,0,0,0]
self.totalKm = UnsafePointer(totalKM_BitsArray).withMemoryRebound(to:UInt64.self, capacity: 1) {$0.pointee}
// 当打印日志给出正确的值时,当在设备上运行时给出错误的 3544649566089386 像这样。
self.totalKm = byte0 | byte1 | byte2 | byte3 | byte4
// 输出是 6900 这和预期的一样正确
解决方案
这种方法存在一些问题:
let data = [UInt8](rowData) // rowData is type of Data class
let totalKM_BitsArray = [data[8], data[7], data[6], data[5], data[4]]
self.totalKm = UnsafePointer(totalKM_BitsArray)
.withMemoryRebound(to:UInt64.self, capacity: 1) { $0.pointee }
取消引用
UnsafePointer(totalKM_BitsArray)
是未定义的行为,因为指向totalKM_BitsArray
's 缓冲区的指针仅在初始化程序调用期间暂时有效(希望在未来的某个时候 Swift 会对此类构造发出警告)。您尝试仅绑定 5 个
UInt8
to实例UInt64
,因此其余 3 个实例将是垃圾。您只能
withMemoryRebound(_:)
在大小和步幅相同的类型之间进行;UInt8
和的情况并非如此UInt64
。这取决于您平台的字节顺序;
data[8]
将是小端平台上的最低有效字节,但大端平台上的最高有效字节。
您的位移实现避免了所有这些问题(并且通常是更安全的方法,因为您不必考虑布局兼容性、对齐和指针别名等问题)。
但是,假设您只想为最重要的字节用零填充数据,rowData[4]
以rowData[8]
弥补其余的不太重要的字节,那么您将希望您的位移实现看起来像这样:
let rowData = Data([
100, 200, 28, 155, 0, 0, 0, 26, 244, 0, 0, 0, 45, 69, 0, 0, 0, 4, 246
])
let byte0 = UInt64(rowData[4]) << 32
let byte1 = UInt64(rowData[5]) << 24
let byte2 = UInt64(rowData[6]) << 16
let byte3 = UInt64(rowData[7]) << 8
let byte4 = UInt64(rowData[8])
let totalKm = byte0 | byte1 | byte2 | byte3 | byte4
print(totalKm) // 6900
或者,迭代地:
var totalKm: UInt64 = 0
for byte in rowData[4 ... 8] {
totalKm = (totalKm << 8) | UInt64(byte)
}
print(totalKm) // 6900
或者,使用reduce(_:_:)
:
let totalKm = rowData[4 ... 8].reduce(0 as UInt64) { accum, byte in
(accum << 8) | UInt64(byte)
}
print(totalKm) // 6900
我们甚至可以将其抽象为一个扩展Data
,以便更容易加载这些固定宽度的整数:
enum Endianness {
case big, little
}
extension Data {
/// Loads the type `I` from the buffer. If there aren't enough bytes to
/// represent `I`, the most significant bits are padded with zeros.
func load<I : FixedWidthInteger>(
fromByteOffset offset: Int = 0, as type: I.Type, endianness: Endianness = .big
) -> I {
let (wholeBytes, spareBits) = I.bitWidth.quotientAndRemainder(dividingBy: 8)
let bytesToRead = Swift.min(count, spareBits == 0 ? wholeBytes : wholeBytes + 1)
let range = startIndex + offset ..< startIndex + offset + bytesToRead
let bytes: Data
switch endianness {
case .big:
bytes = self[range]
case .little:
bytes = Data(self[range].reversed())
}
return bytes.reduce(0) { accum, byte in
(accum << 8) | I(byte)
}
}
}
我们在这里做了一些额外的工作,以便我们读取正确的字节数,以及能够处理大端和小端。但是现在我们已经写好了,我们可以简单地写:
let totalKm = rowData[4 ... 8].load(as: UInt64.self)
print(totalKm) // 6900
请注意,到目前为止,我假设Data
您得到的是零索引。这对于上述示例是安全的,但不一定安全,具体取决于数据的来源(因为它可能是一个切片)。您应该能够Data(someUnknownDataValue)
获得可以使用的零索引数据值,尽管不幸的是我不相信有任何文档可以保证这一点。
为了确保您正确索引任意Data
值,您可以定义以下扩展,以便在处理切片的情况下执行正确的偏移:
extension Data {
subscript(offset offset: Int) -> Element {
get { return self[startIndex + offset] }
set { self[startIndex + offset] = newValue }
}
subscript<R : RangeExpression>(
offset range: R
) -> SubSequence where R.Bound == Index {
get {
let concreteRange = range.relative(to: self)
return self[startIndex + concreteRange.lowerBound ..<
startIndex + concreteRange.upperBound]
}
set {
let concreteRange = range.relative(to: self)
self[startIndex + concreteRange.lowerBound ..<
startIndex + concreteRange.upperBound] = newValue
}
}
}
您可以使用它然后调用例如data[offset: 4]
或data[offset: 4 ... 8].load(as: UInt64.self)
。
最后值得注意的是,虽然您可以通过Data
使用'方法将其实现为对位的重新解释withUnsafeBytes(_:)
:
let rowData = Data([
100, 200, 28, 155, 0, 0, 0, 26, 244, 0, 0, 0, 45, 69, 0, 0, 0, 4, 246
])
let kmData = Data([0, 0, 0] + rowData[4 ... 8])
let totalKm = kmData.withUnsafeBytes { buffer in
UInt64(bigEndian: buffer.load(as: UInt64.self))
}
print(totalKm) // 6900
这依赖于Data
's 缓冲区是 64 位对齐的,这不能保证。尝试加载未对齐的值时会出现运行时错误,例如:
let data = Data([0x01, 0x02, 0x03])
let i = data[1...].withUnsafeBytes { buffer in
buffer.load(as: UInt16.self) // Fatal error: load from misaligned raw pointer
}
通过加载单个UInt8
值并执行位移,我们可以避免此类对齐问题(但是,如果/当UnsafeMutableRawPointer
支持未对齐加载时,这将不再是问题)。
推荐阅读
- python-3.x - 如何在python中将dict列表转换为json?
- sql-server - 将像✅这样的unicode从表单存储到数据库
- javascript - 在 Bootstrap 加载时使用 HMTLWebPackPlugin 测试基于 WebPack 的 WebApp
- machine-learning - 如何处理电影推荐中的发行年份差异
- swift - Swift - 如何为特定节点添加场景边界?
- spring-cloud - Spring Cloud Config 创建什么样的消息队列?
- firebase - Flutter中Firebase一对一聊天的数据结构
- java - 尝试为 mapsforge/vtm 创建 Xamarin Android 绑定 Java 库时出错
- entity-framework - 升级到 Entity Framework Core 3 时出错:当前上下文中不存在 RelationalReferenceCollectionBuilderExtensions
- python - 在 codejam 2019 中提交代码时出现运行时错误