DataKit 提供了一个现代、直观且声明式的接口,用于在 Swift 中读写二进制格式数据。
作为对如何使用此库更轻松地处理二进制格式数据的介绍,首先让我向您介绍我们将要读/写的类型。 假设我们正在构建一个气象站,并且我们使用以下类型来提供有关当前测量值的更新。
struct WeatherStationFeatures: OptionSet, ReadWritable {
var rawValue: UInt8
static var hasTemperature = Self(rawValue: 1 << 0)
static var hasHumidity = Self(rawValue: 1 << 1)
static var usesMetricUnits = Self(rawValue: 1 << 2)
}
struct WeatherStationUpdate {
var features: WeatherStationFeatures
var temperature: Measurement<UnitTemperature>
var humidity: Double
}
编码格式应为:
您有两种选择将上述类型 WeatherStationUpdate
转换为数据:DataBuilder
或 Writable
协议。 如果您打算同时读取和写入数据 - 请务必阅读“读取和写入数据
”部分
DataBuilder
为您提供了一个非常简单且有限的接口。借助结果构建器的强大功能,您可以简单地声明要按给定顺序写入的值,DataBuilder
将接管所有工作,以对值进行编码并将各个字节附加到以形成 Data
对象。 DataBuilder
始终期望返回一个 Data
对象,而不会抛出错误,这就是为什么此处不支持转换 - 您可能需要查看 Writable
协议!
extension WeatherStationUpdate {
@DataBuilder var data: Data {
UInt8(0x02)
features
if features.contains(.hasTemperature) {
Float(temperature.converted(to: features.contains(.usesMetricUnits) ? .celsius : .fahrenheit).value)
}
if features.contains(.hasHumidity) {
UInt8(humidity * 100)
}
CRC32.default
}
}
通过此添加,您可以轻松地使用其 data
属性获取此对象的数据。
借助 keyPaths 和结果构建器的强大功能,您还可以使用 Writable
协议及其 writeFormat
属性将对象写入 Data
。 只需声明单个固定值(例如,字节前缀)、带有 Writable
值的 keyPaths 或本文档“Extras
”部分中进一步解释的其他构造。
extension WeatherStationUpdate: Writable {
static var writeFormat: WriteFormat {
Scope {
UInt8(0x02)
\.features
Using(\.features) { features in
if features.contains(.hasTemperature) {
let unit: UnitTemperature =
features.contains(.usesMetricUnits) ? .celsius : .fahrenheit
Convert(\.temperature) {
$0.converted(to: unit).cast(Float.self)
}
}
if features.contains(.hasHumidity) {
Convert(\.humidity) {
Double($0) / 100
} writing: {
UInt8($0 * 100)
}
}
}
CRC32.default
}
.endianness(.big)
}
}
通过遵循 Writable
协议,您现在可以简单地调用其 write
函数来写出其数据
let message: WeatherStationUpdate = ...
let messageData = try message.write() // You can also inject a custom environment here, if needed
支持将数据读取到对象中稍微复杂一些。 遵循 Readable
协议将要求您实现一个初始化程序,以从给定的 ReadContext
创建对象,并实现一个静态 readFormat
属性来描述数据的对齐方式。
ReadContext
为您提供使用 readFormat
读取的值。 确保在初始化程序和 readFormat
中使用相同的 keyPaths,以确保值的顺利读取。
extension WeatherStationUpdate: Readable {
init(from context: ReadContext<WeatherStationUpdate>) throws {
features = try context.read(for: \.features)
temperature = try context.readIfPresent(for: \.temperature) ?? .init(value: .nan, unit: .kelvin)
humidity = try context.readIfPresent(for: \.humidity) ?? .nan
}
static var readFormat: ReadFormat {
Scope {
UInt8(0x02)
\.features
Using(\.features) { features in
if features.contains(.hasTemperature) {
let unit: UnitTemperature =
features.contains(.usesMetricUnits) ? .celsius : .fahrenheit
Convert(\.temperature) {
$0.converted(to: unit).cast(Float.self)
}
}
if features.contains(.hasHumidity) {
Convert(\.humidity) {
Double($0) / 100
} writing: {
UInt8($0 * 100)
}
}
}
CRC32.default
}
.endianness(.big)
}
}
通过实现 Readable
协议的所有这些要求,您现在可以获得另一个初始化程序 init(_: Data) throws
,以从 Data
对象读取对象
let data: Data = ...
let message = try WeatherStationUpdate(data) // You can also inject a custom environment here, if needed
要使类型同时为 Readable
和 Writable
,您可以使您的类型符合 ReadWritable
协议。 无需为读取和写入提供单独的格式,您可以定义一个 Format
属性,该属性用于读取和写入。 对于我们的示例类型,我们可以简单地将两个格式合并为一个,并提供一个初始化程序以从给定的 ReadContext
创建对象。
extension WeatherStationUpdate: ReadWritable {
init(from context: ReadContext<WeatherStationUpdate>) throws {
features = try context.read(for: \.features)
temperature = try context.readIfPresent(for: \.temperature) ?? .init(value: .nan, unit: .kelvin)
humidity = try context.readIfPresent(for: \.humidity) ?? .nan
}
static var format: Format {
Scope {
UInt8(0x02)
\.features
Using(\.features) { features in
if features.contains(.hasTemperature) {
let unit: UnitTemperature =
features.contains(.usesMetricUnits) ? .celsius : .fahrenheit
Convert(\.temperature) {
$0.converted(to: unit).cast(Float.self)
}
}
if features.contains(.hasHumidity) {
Convert(\.humidity) {
Double($0) / 100
} writing: {
UInt8($0 * 100)
}
}
}
CRC32.default
}
.endianness(.big)
}
}
万岁,您现在可以读取和写入您的对象了! 🎉
读取/写入数据通常非常复杂,并且不同的格式会带来不同的挑战,以最大程度地减少有效负载、减少带宽、提高性能等。为了轻松处理不同的常见场景,DataKit
提供了一些额外的功能来处理最常见的挑战。
在某些特殊情况下,您可能需要更多地控制数据的读取/写入方式。 对于这些情况,包装器 Custom
、Convert
和 Property
可能会引起您的兴趣。
Property
可以轻松包装 keyPath。 您还可以使用其上的函数将 Property
映射到 Custom
或 Convert
包装器。Convert
允许您在读取/写入 keyPath 的值之前对其进行转换。 通常,这对于具有可变大小的序列值非常有用。 您可以直接提供自定义转换方法,也可以使用预先存在的 Conversion
/ReversibleConversion
值。Custom
允许您访问原始读取/写入功能,并可以直接访问 ReadContainer
/WriteContainer
和相应的上下文值。 如果您在代码库中多次需要读取/写入行为,则可能需要查看转换。对于某些类型,没有一个“正确”的格式(例如,考虑 Pascal 与 C 字符串),这就是为什么 DataKit
使用所谓的 Conversion
值来允许定义一次转换然后多次使用。 特别有帮助的是 ReversibleConversion
类型,它允许同时在两个方向上提供转换。
假设我们的类型有一个带有 String
值的 \.string
keyPath,您可以选择使用后缀 0 字节来使用 UTF8 对字符串进行编码(类似于 C 字符串)
Convert(\.string) { // UTF8 string with a suffix 0-byte
$0.encoded(.utf8).dynamicCount
}
.suffix(0 as UInt8)
或者,您可以使用包含字节数的 UTF8
前缀字节来编码字符串(类似于 Pascal ShortString)
Convert(\.string) { // Ascii string with a prefix count byte
$0.encoded(.ascii).prefixCount(UInt8.self)
}
还有更多可用的转换,例如,用于在整数/浮点类型之间进行转换,从而可以轻松地直接转换为您喜欢的类型,而无需自行转换。
使用 Using
构造,您可以访问 ReadContext
中的值或要写入的值。 如果数据本身中的值相互依赖或各个值的读取或写入方式,则 Using
可能非常有用。
与 SwiftUI 的环境类似,您还可以使用 Environment
修改 DataKit
中各个组件的行为。 您可以在启动读取/写入过程时或在 readFormat/writeFormat/format 属性中使用修饰符来修改环境。 要访问环境值,您可能需要查看要在格式构建器之一中使用的 Environment
类型 - 或者为了更直接的访问,ReadContainer
和 WriteContainer
都具有 environment
属性。
以下是一些您可能想要使用的修饰符
endianness
:默认情况下,DataKit
以当前机器的字节序读取和写入值(CRC 除外,其中使用大端)。 如果您的协议需要不同的字节序,请确保指定一个具体的字节序。skipChecksumVerification
:如果您希望在写出数据时创建一个 CRC,但在读取时忽略不正确的校验和值,则可以将此属性设置为 true
- 默认情况下使用 false
。 阅读“校验和
”部分以获取更多信息。suffix
:对于具有动态计数的值(例如,带有 0 后缀字节的值序列),您可以在给定属性上指定 .dynamicCount
转换。 指定的值将停止给定值的读取过程,并且在遇到给定序列的结尾后将写出该值。随意使用 EnvironnentKey
协议和 EnvironmentValues
结构的扩展(与 SwiftUI 非常相似)添加您自己的环境值。
某些构造(例如 CRC 校验和)对整个数据而不是仅对读取/写入特定值的部分进行假设。 通过使用 Scope
,您可以将该数据上下文限制为单独放置它的位置。
例如,假设我们的 WeatherStationUpdate
应该忽略校验和计算的前缀 0x02
字节。 我们可以简单地将其从范围中排除
static var format: Format {
UInt8(0x02)
Scope {
\.features
Using(\.features) { features in
if features.contains(.hasTemperature) {
let unit: UnitTemperature =
features.contains(.usesMetricUnits) ? .celsius : .fahrenheit
Convert(\.temperature) {
$0.converted(to: unit).cast(Float.self)
}
}
if features.contains(.hasHumidity) {
Convert(\.humidity) {
Double($0) / 100
} writing: {
UInt8($0 * 100)
}
}
}
CRC32.default
}
.endianness(.big)
}
通过此更改,仅将在 Scope 本身上验证 CRC,并且前缀字节将被忽略!
DataKit
的依赖项 crc-swift
提供了 CRC 校验和以及一个易于遵循的协议,用于您自己的自定义校验和值。
您可以简单地在其中一个格式构建器中指定校验和值本身。 或者,将 ChecksumProperty
与 keyPath 一起使用,以便您可以将校验和存储在属性中并从属性中编写自定义校验和。 结合 skipChecksumVerification
环境值,您还可以在以后的阶段验证校验和,例如。
DataKit
目前仅支持 Swift 包管理器。
有关如何在您的应用中采用 Swift 包的更多信息,请参见 此 WWDC 演示文稿。
指定 https://github.com/QuickBirdEng/DataKit.git
作为包链接。
如果您不想使用依赖项管理器,则可以通过下载源代码并将文件放置在项目目录中来手动将 DataKit 集成到您的项目中。
DataKit 由 QuickBird 用 ❤️ 创建。
欢迎提出问题以寻求帮助、发现的错误或讨论新的功能请求。 乐于助人! 如果您想建议更改 DataKit,请打开一个拉取请求。
DataKit 在 MIT 许可证下发布。 有关更多信息,请参见 License.md。