【问题标题】:Reading Data into a Struct in Swift在 Swift 中将数据读入结构
【发布时间】:2018-05-11 13:50:10
【问题描述】:

我正在尝试使用withUnsafeBytes 方法将裸Data 读入Swift 4 struct。问题

网络UDP数据包格式如下:

data: 0102 0A00 0000 0B00 0000

01           : 1 byte : majorVersion      (decimal 01)
02           : 1 byte : minorVersion      (decimal 02)
0A00 0000    : 4 bytes: applicationHostId (decimal 10)
0B00 0000    : 4 bytes: versionNumber     (decimal 11)

然后我在Data 上有一个扩展,它需要startlength 字节来读取

extension Data {
    func scanValue<T>(start: Int, length: Int) -> T {
        return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
    }
}

这在逐个读取值时可以正常工作:

// correctly read as decimal "1"
let majorVersion: UInt8 = data.scanValue(start: 0, length: 1)

// correctly read as decimal "2"
let minorVersion: UInt8 = data.scanValue(start: 1, length: 1)

// correctly read as decimal "10"
let applicationHostId: UInt32 = data.scanValue(start: 2, length: 4)

// correctly read as decimal "11"
let versionNumber: UInt32 = data.scanValue(start: 6, length: 4)

然后我创建了一个struct,代表整个数据包如下

struct XPLBeacon {
    var majorVersion: UInt8        // 1 Byte
    var minorVersion: UInt8        // 1 Byte
    var applicationHostId: UInt32  // 4 Bytes
    var versionNumber: UInt32      // 4 Bytes
}

但是当我将数据直接读入结构时,我遇到了一些问题:

var beacon: XPLBeacon = data.scanValue(start: 0, length: data.count)

// correctly read as decimal "1"
beacon.majorVersion

// correctly read as decimal "2"
beacon.minorVersion

// not correctly read
beacon.applicationHostId

// not correctly read
beacon.versionNumber

我应该像这样解析整个结构吗?

【问题讨论】:

  • 请注意,您不需要在方法签名中传递长度。您可以只使用泛型类型的大小func scanValue&lt;T&gt;(start: Int) -&gt; T { return subdata(in: start..&lt;start+MemoryLayout&lt;T&gt;.size).withUnsafeBytes { $0.pointee } }
  • 并将命名start改为at

标签: swift


【解决方案1】:

从数据中读取整个结构不起作用,因为 结构成员填充到它们的自然边界。这 struct XPLBeacon的内存布局是

A B x x C C C C D D D D

在哪里

偏移成员 0 A - 主要版本 (UInt8) 1 B - 次要版本 (UInt8) 2 x x - 填充 4 C C C C - applicationHostId (UInt32) 8 D D D D - 版本号 (UInt32)

并插入填充以便UInt32 成员 对齐的内存地址是其大小的倍数。这是 也证实了

print(MemoryLayout<XPLBeacon>.size) // 12

(有关 Swift 中对齐的更多信息,请参阅 Type Layout)。

如果您将整个数据读入结构,则分配字节 如下

01 02 0A 00 00 00 0B 00 00 00 A B x x C C C C D D D D

这解释了为什么major/minorVersion 是正确的,但applicationHostIdversionNumber 错了。将所有成员与数据分开读取是正确的解决方案。

【讨论】:

  • 我确实注意到MemoryLayout&lt;XPLBeacon&gt;.size 的大小,想知道为什么它是12。这解释了很多。即使创建了很多样板代码,我也会使用自定义初始化程序
【解决方案2】:

由于 Swift 3 数据符合RandomAccessCollectionMutableCollectionRangeReplaceableCollection。因此,您可以简单地创建一个自定义初始化程序来初始化您的结构属性,如下所示:

struct XPLBeacon {
    let majorVersion, minorVersion: UInt8             // 1 + 1 = 2 Bytes
    let applicationHostId, versionNumber: UInt32      // 4 + 4 = 8 Bytes
    init(data: Data) {
        self.majorVersion = data[0]
        self.minorVersion = data[1]
        self.applicationHostId = data
            .subdata(in: 2..<6)
            .withUnsafeBytes { $0.load(as: UInt32.self) }
        self.versionNumber = data
            .subdata(in: 6..<10)
            .withUnsafeBytes { $0.load(as: UInt32.self) }
    }
}

var data = Data([0x01,0x02, 0x0A, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00,0x00])
print(data as NSData)     // "{length = 10, bytes = 0x01020a0000000b000000}\n" <01020a00 00000b00 0000>

let beacon = XPLBeacon(data: data)
beacon.majorVersion       // 1
beacon.minorVersion       // 2
beacon.applicationHostId  // 10
beacon.versionNumber      // 11

【讨论】:

  • 这当然可行,但我试图避免这种情况,因为它会创建很多样板代码,但看起来这是唯一的方法。
  • @LeoDabus,确实,它不能保证数据是正确的,但它确实保证你的 init 在下标范围内不会因致命错误而崩溃应用程序,而是会提供很好的可捕获错误.此外,如果这种类型不是私有样板,最好检查 init 内部,而不是希望调用者之前检查过它。
  • 我没有故意提到我如何获取数据以及它是否有效以更多地关注这个问题。在这里,我们假设数据确实具有正确的大小。
  • @LeoDabus:恐怕我做不到,所以我才说这是一个潜在的问题。 似乎 subdata(in:) 返回对齐的内存,但没有人保证它。 – 在我看来,这使得 load(as:) 方法毫无用处。
  • @LeoDabus:可能,但我不知道!甚至构造函数Data(bytes:count:) 也没有明确声明复制的内存有一些最小对齐。可能是,但目前这似乎是一个实现细节(有点不满意)。
【解决方案3】:

根据 Leo Dabus 的回答,我创建了一个更易读的构造函数:

extension Data {
    func object<T>(at index: Index) -> T {
        subdata(in: index ..< index.advanced(by: MemoryLayout<T>.size))
            .withUnsafeBytes { $0.load(as: T.self) }
    }
}

struct XPLBeacon {
    var majorVersion: UInt8
    var minorVersion: UInt8
    var applicationHostId: UInt32
    var versionNumber: UInt32

    init(data: Data) {
        var index = data.startIndex
        majorVersion = data.object(at: index)

        index += MemoryLayout.size(ofValue: majorVersion)
        minorVersion = data.object(at: index)

        index += MemoryLayout.size(ofValue: minorVersion)
        applicationHostId = data.object(at: index)

        index += MemoryLayout.size(ofValue: applicationHostId)
        versionNumber = data.object(at: index)
    }
}

不属于这部分的当然是检查数据的正确性。正如 cmets 中提到的其他内容,这可以通过具有可失败的 init 方法或通过引发错误来完成。

【讨论】:

  • 我还要声明属性常量。如果您需要更好地更改值以创建新对象
猜你喜欢
  • 1970-01-01
  • 2021-05-30
  • 2023-03-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多