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

Tra*_*ggs 84 swift swift3 swift-data

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

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

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

let input = 42.13 // implicit Double
let bytes = input.data
let roundtrip = bytes.to(Double) // --> 42.13
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*n R 219

注意:现在已经为Swift 4.2/Xcode 10更新了答案(最初为Swift 3编写) ,这使得一些简化成为可能.我之前的一些建议调用了未定义的行为,这已得到修复.我还添加了关于字节顺序和对齐的备注.

如何Data从值创建

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

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

print(data as NSData) // <713d0ad7 a3104540>
Run Code Online (Sandbox Code Playgroud)

说明:

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

在Swift 4.2之前,有必要先创建一个可变副本:

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

如何从中检索值 Data

withUnsafeBytes(_:)有一个Data属性来访问底层存储. UnsafeMutableRawBufferPointer有一个通用的load(fromByteOffset:as:)方法,可以像这样使用:

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
Run Code Online (Sandbox Code Playgroud)

如果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
    }
}
Run Code Online (Sandbox Code Playgroud)

通用解决方案#1

现在可以轻松地将上述转换实现为以下通用方法withUnsafeMutableBytes(of:_:):

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")
}
Run Code Online (Sandbox Code Playgroud)

例:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

同样,您可以将数组转换为copyBytes(to:):

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]
Run Code Online (Sandbox Code Playgroud)

例:

protocol DataConvertible {
    init?(data: Data)
    var data: Data { get }
}
Run Code Online (Sandbox Code Playgroud)

通用解决方案#2

上面的方法有一个缺点: 如何在swift中将double转换为字节数组?,它实际上只适用于"简单"类型,如整数和浮点类型."复杂"类型喜欢DataProtocolData拥有(隐藏)指向底层存储的指针,并且不能通过复制结构本身来传递.它也不适用于只是指向真实对象存储的引用类型.

可以解决这个问题

  • 定义一个协议,定义转换为copyBytes()和返回的方法:

    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) }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 在协议扩展中将转换实现为默认方法:

    extension Int : DataConvertible { }
    extension Float : DataConvertible { }
    extension Double : DataConvertible { }
    // add more types here ...
    
    Run Code Online (Sandbox Code Playgroud)

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

  • 最后声明符合所有可以安全转换struct Data回来的类型:

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

这使得转换更加优雅:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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
}
Run Code Online (Sandbox Code Playgroud)

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

进一步的评论

字节顺序

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

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

print(data as NSData) // <713d0ad7 a3104540>
Run Code Online (Sandbox Code Playgroud)

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

对准

从上面提取值的上述方法T: ExpressibleByIntegerLiteral假设数据已正确对齐值类型.如果无法保证, Data可以使用创建具有对齐的底层存储的副本.

  • @HansBrende:恐怕目前无法做到.它需要一个`扩展Array:DataConvertible,其中Element:DataConvertible`.这在Swift 3中是不可能的,但是计划用于Swift 4(据我所知).比较https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#conditional-conformances-中的"条件一致性" - (2认同)