字节

Test Status

Bytes 旨在成为你首选的 Swift 库,用于将基本类型转换为字节缓冲区,以及从字节缓冲区转换回基本类型,以便进行序列化并写入网络、文件或数据库。

快速链接

安装

在你的 Package.swift 文件中添加 Bytes 作为依赖项即可开始使用。然后,在你希望使用该库的任何文件中添加 import Bytes

请查看发布版本以获取推荐版本。

dependencies: [
    .package(url: "https://github.com/mochidev/Bytes.git", .upToNextMinor(from: "0.3.1")),
],
...
targets: [
    .target(
        name: "MyPackage",
        dependencies: [
            "Bytes",
        ]
    )
]

什么是 Bytes

Bytes[UInt8] 的类型别名,这意味着你可以在任何需要字节缓冲区的地方使用它。虽然提供了 IntStringUUID 甚至枚举的扩展,但这个库最终允许你将任何固定大小的结构转换为 Bytes 以及从 Bytes 转换回来。

struct Example {
    let a: UInt8
    let b: UInt8
}

func methodThatTakesExample(_ example: Example) { ... }

let example = Example(a: 1, b: 2)

// Turn a Swift type into Bytes:
let bytes = Bytes(casting: example) // [0x01, 0x02]

// Turn Bytes into a Swift type:
let backToExample = try bytes.casting(to: Example.self) // Type is explicit
methodThatTakesExample(try bytes.casting()) // Type is inferred from context

请注意,以这种方式转换为 Bytes 和从 Bytes 转换回来的对象在程序范围之外是不稳定的,因为它们反映了对象在那个时刻的内存布局。更可取的方法是直接使用 IntStringUUID 等的专用方法来编码项目,因为它们提供了一种稳定且一致的内存中对象编码和解码方案。

整数

当处理整数时,如果可能,最好使用 bigEndianBytes/ littleEndianBytesinit(bigEndianBytes: Bytes)/init(littleEndianBytes: Bytes) 这些整数特定的属性和初始化器,因为它们可以保证跨平台(类型转换将始终使用系统内存的原生字节序)。整数重载适用于所有整数类型:Int8UInt8Int16UInt16Int32UInt32Int64UInt64。不建议使用 IntUInt 进行序列化,因为它们在不同平台上的大小不同。

字符串

为了使处理字符串和字符变得简单明了,提供了 utf8Bytesinit(utf8Bytes: Bytes)。这些不是空字符结尾的,但你可以很容易地实现这一点:"Hello".utf8Bytes + [0]

UUID

UUID 支持以紧凑字节形式 (bytesinit(bytes: Bytes)) 和人类可读的字符串形式 (stringBytesinit(stringBytes: Bytes)) 进行序列化。

枚举、选项集合和 RawRepresentable

枚举和符合 RawRepresentable 的其他类型也通过 rawBytesinit(rawBytes: Bytes) 开箱即用地支持。RawType 为整数或字符串/字符的类型也可以使用上述 getter 和初始化器。

整数、UUID、枚举和其他固定大小类型的集合

整数的数组或集合可以使用 bigEndianBytes/ littleEndianBytes 进行序列化,并使用 init(bigEndianBytes: Bytes)/init(littleEndianBytes: Bytes) 进行初始化。对于 UUID 和 RawRepresentable 枚举的集合,也可以通过使用特定于其类型的 API 来完成相同的操作。

检查值

有时,在读取字节序列时,你只想验证序列中的下一个字节是否为常量。这可以使用迭代器上的 check() 系列方法轻松完成

try iterator.check(0) // Check for 0x00, throw if not found
try iterator.check([0x0d, 0x0a]) // Check for \r\n, throw if not found
try iterator.checkIfPresent([0x0d, 0x0a]) // Check for \r\n, return false if iterator is finished, throw if not finished and not found
try iterator.checkIfPresent(utf8: "\r\n") // Check for \r\n, return false if iterator is finished, throw if not finished and not found
try iterator.checkIfPresent(utf8: Separators.header) // Check for \r\n\r\n, return false if iterator is finished, throw if not finished and not found

复杂示例

由于 Bytes 只是一个数组,因此所有你习惯使用的方法都可用,包括使用切片。如果你正在处理更复杂的序列化,请考虑以下示例

struct Versionstamp {
    let transactionCommitVersion: UInt64
    let batchNumber: UInt16
    var userData: UInt16?
    
    init(transactionCommitVersion: UInt64, batchNumber: UInt16, userData: UInt16? = nil) {
        self.transactionCommitVersion = transactionCommitVersion
        self.batchNumber = batchNumber
        self.userData = userData
    }

    var bytes: Bytes {
        var result = Bytes()
        result.reserveCapacity(userData == nil ? 10 : 12)
        
        result.append(contentsOf: transactionCommitVersion.bigEndianBytes)
        result.append(contentsOf: batchNumber.bigEndianBytes)
        
        if let userData = userData {
            result.append(contentsOf: userData.bigEndianBytes)
        }
        
        return result
    }
    
    init<Bytes: BytesCollection>(_ bytes: Bytes?) throws {
        guard let bytes = bytes, bytes.count == 10 || bytes.count == 12 else {
            throw Error.invalidVersionstamp
        }
        
        let transactionCommitVersion = try! UInt64(bigEndianBytes: bytes[0..<8])
        let batchNumber = try! UInt16(bigEndianBytes: bytes[8..<10])
        var userData: UInt16?
        
        if bytes.count == 12 {
            userData = try! UInt16(bigEndianBytes: bytes[10..<12])
        }
        
        self.init(transactionCommitVersion: transactionCommitVersion, batchNumber: batchNumber, userData: userData)
    }
    
    /// Alternatively using the ByteIterator-class of methods, which don't require keeping track of indices since they decode objects in order.
    init<Bytes: BytesCollection>(sequence: Bytes?) throws {
        var iterator = sequence.makeIterator()
                        
        self.init(
            transactionCommitVersion: try iterator.next(bigEndian: UInt64.self),
            batchNumber: try iterator.next(bigEndian: UInt16.self),
            userData: try iterator.nextIfPresent(bigEndian: UInt16.self)
        )
        
        /// Verify we are at the end of the stream of bytes.
        guard iterator.next() == nil else {
            throw Error.invalidVersionstamp
        }
    }
}

AsyncSequence

Bytes 也可以用于从 AsyncSequence 迭代器中提取数据。要了解更多信息,请参阅与 AsyncSequenceReader 集成

例如,改进上述示例

#if compiler(>=5.5) && canImport(_Concurrency)

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension AsyncIteratorProtocol where Element == Byte {
    @inlinable
    mutating func next(_ type: Versionstamp.Type) async throws -> Versionstamp {
        Versionstamp(
            transactionCommitVersion: try await next(bigEndian: UInt64.self),
            batchNumber: try await next(bigEndian: UInt16.self),
            userData: try await nextIfPresent(bigEndian: UInt16.self)
        )
    }
}

#endif

贡献

欢迎贡献!请查看已有的 issue,或开始一个新的 issue 来讨论新功能。尽管不能保证满足功能请求,但符合项目目标并且事先讨论过的 PR 非常受欢迎!

请确保所有提交都具有清晰的提交历史记录,文档齐全且经过全面测试。请在提交之前 rebase 你的 PR,而不是合并到 main 分支。需要线性历史记录,因此 PR 中的合并提交将不被接受。