首页 文章

往返数据的往返Swift数字类型

提问于
浏览
71

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

this answer for using [UInt8],但它似乎使用了我在Data上找不到的各种指针API .

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

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

真正逃避我的部分,我已经查看了一堆文档,我是如何从任何基本结构(所有数字都是)得到某种指针的东西(OpaquePointer或BufferPointer或UnsafePointer?) . 在C中,我只是在它前面拍一个&符号,然后就去了 .

3 回答

  • 2

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

    如何从值创建数据

    从Swift 4.2开始,可以使用简单的值创建数据

    let value = 42.13
    let data = withUnsafeBytes(of: value) { Data($0) }
    
    print(data as NSData) // <713d0ad7 a3104540>
    

    说明:

    • withUnsafeBytes(of: value)使用覆盖值的原始字节的缓冲区指针调用闭包 .

    • 原始缓冲区指针是一个字节序列,因此Data($0)可用于创建数据 .

    如何从Data中检索值

    从Swift 5开始, DatawithUnsafeBytes(_:)用字节的“无类型” UnsafeMutableRawBufferPointer 调用闭包 . 通过将内存“绑定”到所需的值类型来获得“类型”指针,最后取消引用类型指针:

    let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
    let value = data.withUnsafeBytes {
        $0.baseAddress!.bindMemory(to: Double.self, capacity: 1).pointee
    }
    print(value) // 42.13
    

    这种方法存在一个问题:将内存绑定到 Double (或任何其他类型)要求它是针对该类型的属性对齐(此处:对齐到8字节地址) . 但这并不能保证,例如如果数据是作为另一个 Data 值的切片获得的 .

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

    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
    

    说明:

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

    通用解决方案#1

    现在可以轻松地将上述转换实现为 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”(整数和浮点)类型一起使用,见下文 .

    例:

    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 并返回:

    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

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

    可以解决这个问题

    • 定义一个协议,定义转换为 Data 和返回的方法:
    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
    
    • 在协议扩展中将转换实现为默认方法:
    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) }
        }
    }
    

    我在这里选择了一个可用的初始化器,它检查提供的字节数是否与类型的大小相匹配 .

    • 最后声明符合所有可以安全转换为 Data 并返回的类型:
    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
    }
    

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

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

    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)
        }
    }
    

    或者在您自己的类型中实现转换方法以执行必要的操作,以便序列化和反序列化值 .

    字节顺序

    在上述方法中没有进行字节顺序转换,数据总是按主机字节顺序排列 . 对于平台无关表示(例如“大端”,也称为“网络”字节顺序),使用相应的整数属性resp . 初始化 . 例如:

    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
    }
    

    当然,这种转换也可以在通用中进行转换方法 .

  • 185

    您可以使用withUnsafePointer获取指向 mutable 对象的不安全指针:

    withUnsafePointer(&input) { /* $0 is your pointer */ }
    

    我不知道为不可变对象获取一个的方法,因为inout运算符只适用于可变对象 .

    这与你所链接的答案有关 .

  • 3

    就我而言,_2618196的答案有所帮助,但结果却被颠倒了 . 所以我对他的代码进行了一些小改动:

    extension UInt16 : DataConvertible {
    
        init?(data: Data) {
            guard data.count == MemoryLayout<UInt16>.size else { 
              return nil 
            }
        self = data.withUnsafeBytes { $0.pointee }
        }
    
        var data: Data {
             var value = CFSwapInt16HostToBig(self)//Acho que o padrao do IOS 'e LittleEndian, pois os bytes estavao ao contrario
             return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
        }
    }
    

    这个问题与LittleEndian和BigEndian有关 .

相关问题