往返 Swift 数字类型到/从数据 [英] round trip Swift number types to/from Data
问题描述
随着 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:_:)
使用覆盖值的原始字节的可变缓冲区指针调用闭包.copyBytes(to:)
DataProtocol
的方法(Data
符合)将字节从数据复制到该缓冲区.
withUnsafeMutableBytes(of:_:)
invokes the closure with a mutable buffer pointer covering the raw bytes of the value.- The
copyBytes(to:)
method ofDataProtocol
(to whichData
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
上述方法有一个缺点:它实际上只适用于琐碎"类型,如整数和浮点类型.复杂"类型,如 Array
和 String
有(隐藏的)指向底层存储的指针,不能被仅通过复制结构本身来传递.它也不会与引用类型只是指向实际对象存储的指针.
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屋!