注意: 代码已经更新到 Swift 5 (Xcode 10.2)。 (Swift 3 和 Swift 4.2 版本可以在编辑历史中找到。) 同时,可能存在未对齐的数据现在已经得到正确处理。
如何从值创建 Data
从 Swift 4.2 开始,可以使用以下方式简单地从值创建数据:
let value = 42.13
let data = withUnsafeBytes(of: value) { Data($0) }
print(data as NSData) // <713d0ad7 a3104540>
说明:
如何从Data
中检索值
在Swift 5中,Data
的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)
这种方法存在一个问题:它要求内存对于该类型是正确地对齐(这里是对齐到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)
解释:
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
,以便我们可以轻松地将值初始化为“零” - 这实际上并不是一种限制,因为该方法仍可用于“平凡”的(整数和浮点数)类型,详见下文。
示例:
let value = 42.13
let data = Data(from: value)
print(data as NSData)
if let roundtrip = data.to(type: Double.self) {
print(roundtrip)
} 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)
let roundtrip = data.toArray(type: Int16.self)
print(roundtrip)
通用解决方案 #2
以上方法有一个缺点:它实际上只适用于像整数和浮点类型这样的“简单”类型。像 Array
和 String
这样的“复杂”类型具有(隐藏的)指向底层存储的指针,不能仅通过复制结构体本身来传递。它也无法处理只是指向真实对象存储的指针的引用类型。
为了解决这个问题,可以:
Define a protocol which defines the methods for converting to Data
and back:
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) }
}
}
I have chosen a failable initializer here which checks that the number of bytes provided
matches the size of the type.
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 { }
这使得转换变得更加优雅:
let value = 42.13
let data = value.data
print(data as NSData)
if let roundtrip = Double(data: data) {
print(roundtrip)
}
第二种方法的优点是您不会意外地进行不安全的转换。缺点是您必须明确列出所有“安全”的类型。
您还可以为其他需要非平凡转换的类型实现协议,例如:
extension String: DataConvertible {
init?(data: Data) {
self.init(data: data, encoding: .utf8)
}
var data: Data {
return Data(self.utf8)
}
}
或者在您自己的类型中实现转换方法,以执行必要的操作来序列化和反序列化值。
字节顺序
上述方法中不进行任何字节顺序转换,数据始终处于主机字节顺序。为了获得平台无关的表示(例如“大端”或“网络”字节顺序),请使用相应的整数属性或初始化程序。例如:
let value = 1000
let data = value.bigEndian.data
print(data as NSData)
if let roundtrip = Int(data: data) {
print(Int(bigEndian: roundtrip))
}
当然,这种转换也可以在通用的转换方法中完成。