在单个数据实例中存储多个不同类型对象的数据表示

Storing the data representations of multiple, differently typed objects in a single Data instance

动机

据我所知,Data 是一个抽象字节缓冲区的结构。它引用内存中的物理区域,换句话说:连续的字节数。现在我想有效地将​​多个值存储在内存中(作为原始数据),其中这些值 并非都是同一类型

我在这里对高效的定义≔ 存储所有这些值,没有任何未使用的缓冲区/间隙字节。

将原始数据存储在内存中

let a: UInt8 = 39
let b: Int32 = -20001
let string: String = "How awesome is this data?!"

现在我想将所有这些值的数据按顺序存储在内存中,没有任何类型信息。

let data = [a.asData, b.asData, string.asData].concatenated()

假设 .asData 属性 将每个实例的字节表示形式检索为 [UInt8] 数组,然后将它们包装在 Data 实例中。 concetenated() 方法然后将这 3 个 Data 实例连接到单个 Data 实例,如下所示:

extension Collection where Element == Data {
    func concatenated() -> Data {
        reduce(into: Data()) { (result, nextDataChunk) in
            result.append(nextDataChunk)
        }
    }
}

从内存中将数据读回相应的类型

让我们假设这一切都很好,我现在有一个 Data 实例,我想从中恢复 3 个原始值(及其原始类型)。这就是我所做的:

var cursor = 0

let a: UInt8 = data.withUnsafeBytes { pointer in
    pointer.load(fromByteOffset: cursor, as: UInt8.self)
}
cursor += MemoryLayout<UInt8>.size // +1

let b: Int32 = data.withUnsafeBytes { pointer in
    pointer.load(fromByteOffset: cursor, as: Int32.self)
}
cursor += MemoryLayout<Int32>.size // +4

let string: String = data.withUnsafeBytes { pointer in
    pointer.load(fromByteOffset: cursor, as: String.self)
}
cursor += MemoryLayout<String>.size // +16

问题

问题是这会引发运行时错误:

Fatal error: load from misaligned raw pointer

我很清楚为什么:

Int32 对齐为 4(因为它有 4 个字节长)。换句话说:当使用原始指针读取数据时,Int32 的第一个字节必须位于索引是 4 的倍数的位置。但由于第一个值只是 UInt8,因此数据Int32 的字节从索引 1 开始,它 不是 4 的倍数。因此,我得到了错误。


我的问题是:

关于未对齐数据的问题是您需要使用数据的子数据方法。除此之外,您还可以创建一些助手来让您的生活更轻松,如下所示:

这会将任何数字类型转换为数据:

extension Numeric {
    var data: Data {
        var bytes = self
        return .init(bytes: &bytes, count: MemoryLayout<Self>.size)
    }
}

这会将任何符合字符串协议的类型转换为数据 (String/Substring)

extension StringProtocol {
    var data: Data { .init(utf8) }
}

这会将任何有效的 utf8 编码字节序列 (UInt8) 转换为字符串

extension DataProtocol {
    var string: String? { String(bytes: self, encoding: .utf8) }
}

这是将字节转换为对象或对象集合(数组)的通用方法:

extension ContiguousBytes {
    func object<T>() -> T { withUnsafeBytes { [=13=].load(as: T.self) } }
    func objects<T>() -> [T] { withUnsafeBytes { .init([=13=].bindMemory(to: T.self)) } }
}

以及用于连接数据数组的简化通用版本:

extension Collection where Element == DataProtocol {
    var data: Data { .init(joined()) }
}

用法:

let a: UInt8 = 39
let b: Int32 = -20001
let string: String = "How awesome is this data?!"
let data = [a.data, b.data, string.data].data

// just set the cursor (index) at the start position
var cursor = data.startIndex
// get the subdata from that position onwards
let loadedA: UInt8 = data.subdata(in: cursor..<data.endIndex).object()  // 39
// advance your cursor for the next position
cursor = cursor.advanced(by: MemoryLayout<UInt8>.size)
// get your next object
let loadedB: Int32 = data.subdata(in: cursor..<data.endIndex).object()  // -20001
// advance your position to the start of the string data
cursor = cursor.advanced(by: MemoryLayout<Int32>.size)
// load the subdata as string
let loadedString = data.subdata(in: cursor..<data.endIndex).string  // "How awesome is this data?!"

edit/update: 当然加载字符串只有效,因为它位于字节集合的末尾,否则你需要使用 8 个字节来存储它的大小:

let a: UInt8 = 39
let b: Int32 = -20001
let string: String = "How awesome is this data?!"
let c: Int = .max
let data = [a.data, b.data, string.count.data, string.data, c.data].data

var cursor = data.startIndex
let loadedA: UInt8 = data.subdata(in: cursor..<data.endIndex).object()  // 39
print(loadedA)
cursor = cursor.advanced(by: MemoryLayout<UInt8>.size)
let loadedB: Int32 = data.subdata(in: cursor..<data.endIndex).object()  // -20001
print(loadedB)
cursor = cursor.advanced(by: MemoryLayout<Int32>.size)
let stringCount: Int = data.subdata(in: cursor..<data.endIndex).object()
print(stringCount)
cursor = cursor.advanced(by: MemoryLayout<Int>.size)
let stringEnd = cursor.advanced(by: stringCount)

if let loadedString = data.subdata(in: cursor..<stringEnd).string {  // "How awesome is this data?!"
    print(loadedString)
    cursor = stringEnd
    let loadedC: Int = data.subdata(in: cursor..<data.endIndex).object()  // 9223372036854775807
    print(loadedC)
}

这将打印

39
-20001
26
How awesome is this data?!
9223372036854775807