使用Swift 3Data而不是[UInt8],我试图找出最有效/惯用的编码/解码方式,将各种数字类型(UInt8,Double,Float,Int64等)转换为Data对象。
Data
[UInt8]
有这个答案使用[UINT8] ,但它似乎是使用各种指针的API,我不能对数据找到。
我基本上想要一些类似于以下内容的自定义扩展:
let input = 42.13 // implicit Double let bytes = input.data let roundtrip = bytes.to(Double) // --> 42.13
我仔细阅读了许多文档,真正使我难以理解的部分是如何从任何基本结构(所有数字都是)中获得某种指针(OpaquePointer或BufferPointer或UnsafePointer?)。在C语言中,我只需在它前面拍一个&符,然后就可以了。
注意: 该代码现已针对 Swift 5 (Xcode 10.2)更新。(可以在编辑历史记录中找到Swift 3和Swift4.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)
从Swift 5开始,withUnsafeBytes(_:)of会Data使用UnsafeMutableRawBufferPointer对字节的“未类型化” 来调用闭包。该load(fromByteOffset:as:)方法从内存中读取值:
withUnsafeBytes(_:)
UnsafeMutableRawBufferPointer
load(fromByteOffset:as:)
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值的切片获得的。
因此, 将 字节 复制 到以下值更安全:
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:_:)
copyBytes(to:)
DataProtocol
返回值copyBytes()是复制的字节数。它等于目标缓冲区的大小,如果数据不包含足够的字节,则等于或小于目标缓冲区的大小。
copyBytes()
上面的转换现在可以很容易地实现为以下方法的通用方法struct Data:
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添加了约束,以便我们可以轻松地将值初始化为“零” –这并不是真正的限制,因为无论如何该方法都可以与“特制”(整数和浮点)类型一起使用,请参见下文。
T: ExpressibleByIntegerLiteral
例:
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]
上面的方法有一个缺点:它实际上仅适用于“平凡”类型,例如整数和浮点类型。“复杂”类型喜欢Array 和String具有指向基础存储的(隐藏)指针,并且不能仅通过复制结构本身来传递。它也不适用于引用类型,引用类型只是指向实际对象存储的指针。
Array
String
所以解决这个问题,一个可以
定义一个协议,该协议定义了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) } }
我在这里选择了一个 失败的 初始化程序,该初始化程序检查提供的字节数是否与类型的大小匹配。
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) } }
或以自己的类型实现转换方法以执行所需的任何操作,以便对值进行序列化和反序列化。
在上述方法中,没有字节顺序转换,数据始终以主机字节顺序进行。对于平台无关的表示形式(例如“大端”或“网络”字节顺序),请使用相应的整数属性。初始化程序。例如:
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 }
当然,这种转换通常也可以用通用转换方法来完成。