往返 Swift 数字类型到/从数据 [英] round trip Swift number types to/from Data

查看:25
本文介绍了往返 Swift 数字类型到/从数据的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

随着 Swift 3 倾向于 Data 而不是 [UInt8],我试图找出编码/解码各种数字的最有效/惯用的方法类型(UInt8、Double、Float、Int64 等)作为数据对象.

With Swift 3 leaning towards Data instead of [UInt8], I'm trying to ferret out what the most efficient/idiomatic way to encode/decode swifts various number types (UInt8, Double, Float, Int64, etc) as Data objects.

这个使用 [UInt8 的答案],但它似乎使用了我在 Data 上找不到的各种指针 API.

There's this answer for using [UInt8], but it seems to be using various pointer APIs that I can't find on Data.

我想要一些基本的自定义扩展,看起来像:

I'd like to basically some custom extensions that look something like:

let input = 42.13 // implicit Double
let bytes = input.data
let roundtrip = bytes.to(Double) // --> 42.13

真正让我望而却步的部分,我浏览了一堆文档,是我如何从任何基本结构(所有数字都是).在 C 语言中,我会在它前面加上一个 & 符号,然后就可以了.

The part that really eludes me, I've looked through a bunch of the docs, is how I can get some sort of pointer thing (OpaquePointer or BufferPointer or UnsafePointer?) from any basic struct (which all of the numbers are). In C, I would just slap an ampersand in front of it, and there ya go.

推荐答案

注意:Swift 5 (Xcode 10.2) 的代码现已更新.(Swift 3 和 Swift 4.2 版本可以在编辑历史记录中找到.)现在还可能正确处理了未对齐的数据.

Note: The code has been updated for Swift 5 (Xcode 10.2) now. (Swift 3 and Swift 4.2 versions can be found in the edit history.) Also possibly unaligned data is now correctly handled.

从 Swift 4.2 开始,可以简单地从一个值创建数据

As of Swift 4.2, data can be created from a value simply with

let value = 42.13
let data = withUnsafeBytes(of: value) { Data($0) }

print(data as NSData) // <713d0ad7 a3104540>

说明:

  • withUnsafeBytes(of: value)使用覆盖值的原始字节的缓冲区指针调用闭包.
  • 原始缓冲区指针是一个字节序列,因此 Data($0) 可用于创建数据.

从 Swift 5 开始,withUnsafeBytes(_:)Data 的 code> 调用带有无类型"UnsafeMutableRawBufferPointer 到字节的闭包.load(fromByteOffset:as:)从内存中读取值的方法:

As of Swift 5, the withUnsafeBytes(_:) of Data invokes the closure with an "untyped" UnsafeMutableRawBufferPointer to the bytes. The load(fromByteOffset:as:) method the reads the value from the memory:

let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes {
    $0.load(as: Double.self)
}
print(value) // 42.13

这种方法有一个问题:它要求内存是类型的属性对齐(这里:对齐到 8 字节地址).但这并不能保证,例如如果数据是作为另一个 Data 值的切片获得的.

There is one problem with this approach: It requires that the memory is property aligned for the type (here: aligned to a 8-byte address). But that is not guaranteed, e.g. if the data was obtained as a slice of another Data value.

因此将字节复制到值更安全:

It is therefore safer to copy the bytes to the value:

let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
var value = 0.0
let bytesCopied = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
assert(bytesCopied == MemoryLayout.size(ofValue: value))
print(value) // 42.13

说明:

  • withUnsafeMutableBytes(of:_:) invokes the closure with a mutable buffer pointer covering the raw bytes of the value.
  • The copyBytes(to:) method of DataProtocol (to which Data conforms) copies bytes from the data to that buffer.

copyBytes() 的返回值是复制的字节数.它等于目标缓冲区的大小,如果数据包含的字节数不足,则小于.

The return value of copyBytes() is the number of bytes copied. It is equal to the size of the destination buffer, or less if the data does not contain enough bytes.

上述转换现在可以轻松实现为struct Data的通用方法:

The above conversions can now easily be implemented as generic methods of struct Data:

extension Data {

    init<T>(from value: T) {
        self = Swift.withUnsafeBytes(of: value) { Data($0) }
    }

    func to<T>(type: T.Type) -> T? where T: ExpressibleByIntegerLiteral {
        var value: T = 0
        guard count >= MemoryLayout.size(ofValue: value) else { return nil }
        _ = Swift.withUnsafeMutableBytes(of: &value, { copyBytes(to: $0)} )
        return value
    }
}

约束 T: ExpressibleByIntegerLiteral 在这里添加,以便我们可以轻松地将值初始化为零"——这不是真正的限制,因为此方法可以与trival"(整数和浮点数)类型,见下文.

The constraint T: ExpressibleByIntegerLiteral is added here so that we can easily initialize the value to "zero" – that is not really a restriction because this method can be used with "trival" (integer and floating point) types anyway, see below.

示例:

let value = 42.13 // implicit Double
let data = Data(from: value)
print(data as NSData) // <713d0ad7 a3104540>

if let roundtrip = data.to(type: Double.self) {
    print(roundtrip) // 42.13
} else {
    print("not enough data")
}

同样,您可以将数组转换为Data并返回:

Similarly, you can convert arrays to Data and back:

extension Data {

    init<T>(fromArray values: [T]) {
        self = values.withUnsafeBytes { Data($0) }
    }

    func toArray<T>(type: T.Type) -> [T] where T: ExpressibleByIntegerLiteral {
        var array = Array<T>(repeating: 0, count: self.count/MemoryLayout<T>.stride)
        _ = array.withUnsafeMutableBytes { copyBytes(to: $0) }
        return array
    }
}

示例:

let value: [Int16] = [1, Int16.max, Int16.min]
let data = Data(fromArray: value)
print(data as NSData) // <0100ff7f 0080>

let roundtrip = data.toArray(type: Int16.self)
print(roundtrip) // [1, 32767, -32768]

通用解决方案#2

上述方法有一个缺点:它实际上只适用于琐碎"类型,如整数和浮点类型.复杂"类型,如 ArrayString 有(隐藏的)指向底层存储的指针,不能被仅通过复制结构本身来传递.它也不会与引用类型只是指向实际对象存储的指针.

Generic solution #2

The above approach has one disadvantage: It actually works only with "trivial" types like integers and floating point types. "Complex" types like Array and String have (hidden) pointers to the underlying storage and cannot be passed around by just copying the struct itself. It also would not work with reference types which are just pointers to the real object storage.

解决这个问题,就可以了

So solve that problem, one can

  • 定义一个协议,该协议定义了转换为Data和返回的方法:

protocol DataConvertible {
    init?(data: Data)
    var data: Data { get }
}

  • 将转换实现为协议扩展中的默认方法:

  • Implement the conversions as default methods in a protocol extension:

    extension DataConvertible where Self: ExpressibleByIntegerLiteral{
    
        init?(data: Data) {
            var value: Self = 0
            guard data.count == MemoryLayout.size(ofValue: value) else { return nil }
            _ = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
            self = value
        }
    
        var data: Data {
            return withUnsafeBytes(of: self) { Data($0) }
        }
    }
    

    我在这里选择了一个 failable 初始化程序,它检查提供的字节数匹配类型的大小.

    I have chosen a failable initializer here which checks that the number of bytes provided matches the size of the type.

    最后声明对所有可以安全转换为Data并返回的类型的一致性:

    And finally declare conformance to all types which can safely be converted to Data and back:

    extension Int : DataConvertible { }
    extension Float : DataConvertible { }
    extension Double : DataConvertible { }
    // add more types here ...
    

  • 这使得转换更加优雅:

    let value = 42.13
    let data = value.data
    print(data as NSData) // <713d0ad7 a3104540>
    
    if let roundtrip = Double(data: data) {
        print(roundtrip) // 42.13
    }
    

    第二种方法的优点是您不会无意中进行不安全的转换.缺点是您必须明确列出所有安全"类型.

    The advantage of the second approach is that you cannot inadvertently do unsafe conversions. The disadvantage is that you have to list all "safe" types explicitly.

    您还可以为需要非平凡转换的其他类型实现协议,例如:

    You could also implement the protocol for other types which require a non-trivial conversion, such as:

    extension String: DataConvertible {
        init?(data: Data) {
            self.init(data: data, encoding: .utf8)
        }
        var data: Data {
            // Note: a conversion to UTF-8 cannot fail.
            return Data(self.utf8)
        }
    }
    

    或者在你自己的类型中实现转换方法来做任何事情必要的所以序列化和反序列化一个值.

    or implement the conversion methods in your own types to do whatever is necessary so serialize and deserialize a value.

    以上方法没有做字节序转换,数据总是在主机字节顺序.对于独立于平台的表示(例如big endian"又名network"字节顺序),使用相应的整数属性初始化程序.例如:

    No byte order conversion is done in the above methods, the data is always in the host byte order. For a platform independent representation (e.g. "big endian" aka "network" byte order), use the corresponding integer properties resp. initializers. For example:

    let value = 1000
    let data = value.bigEndian.data
    print(data as NSData) // <00000000 000003e8>
    
    if let roundtrip = Int(data: data) {
        print(Int(bigEndian: roundtrip)) // 1000
    }
    

    当然这个转换一般也可以做,在泛型中转换方法.

    Of course this conversion can also be done generally, in the generic conversion method.

    这篇关于往返 Swift 数字类型到/从数据的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆