Nota: El código se ha actualizado para Swift 5 (Xcode 10.2) ahora. (Las versiones Swift 3 y Swift 4.2 se pueden encontrar en el historial de ediciones). También los datos posiblemente no alineados ahora se manejan correctamente.
Cómo crear a Data
partir de un valor
A partir de Swift 4.2, los datos se pueden crear a partir de un valor simplemente con
let value = 42.13
let data = withUnsafeBytes(of: value) { Data($0) }
print(data as NSData) // <713d0ad7 a3104540>
Explicación:
withUnsafeBytes(of: value)
invoca el cierre con un puntero de búfer que cubre los bytes sin procesar del valor.
- Un puntero de búfer sin formato es una secuencia de bytes, por lo que
Data($0)
se puede utilizar para crear los datos.
Cómo recuperar un valor de Data
A partir de Swift 5, withUnsafeBytes(_:)
de Data
invoca el cierre con un "sin tipo" UnsafeMutableRawBufferPointer
en los bytes. El load(fromByteOffset:as:)
método lee el valor de la memoria:
let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes {
$0.load(as: Double.self)
}
print(value) // 42.13
Hay un problema con este enfoque: requiere que la memoria esté alineada con las propiedades del tipo (aquí: alineada con una dirección de 8 bytes). Pero eso no está garantizado, por ejemplo, si los datos se obtuvieron como una porción de otro Data
valor.
Por tanto, es más seguro copiar los bytes al valor:
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
Explicación:
withUnsafeMutableBytes(of:_:)
invoca el cierre con un puntero de búfer mutable que cubre los bytes sin procesar del valor.
- El
copyBytes(to:)
método de DataProtocol
(con el que se Data
ajusta) copia bytes de los datos a ese búfer.
El valor de retorno de copyBytes()
es el número de bytes copiados. Es igual al tamaño del búfer de destino, o menor si los datos no contienen suficientes bytes.
Solución genérica n. ° 1
Las conversiones anteriores ahora se pueden implementar fácilmente como métodos genéricos de 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
}
}
La restricción T: ExpressibleByIntegerLiteral
se agrega aquí para que podamos inicializar fácilmente el valor a "cero"; eso no es realmente una restricción porque este método se puede usar con tipos "trival" (entero y punto flotante) de todos modos, ver más abajo.
Ejemplo:
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")
}
Del mismo modo, puede convertir matrices de Data
ida y vuelta:
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
}
}
Ejemplo:
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]
Solución genérica n. ° 2
El enfoque anterior tiene una desventaja: en realidad, solo funciona con tipos "triviales" como enteros y tipos de coma flotante. Tipos "complejos" como Array
yString
tienen punteros (ocultos) al almacenamiento subyacente y no se pueden pasar simplemente copiando la estructura en sí. Tampoco funcionaría con tipos de referencia que son solo punteros al almacenamiento de objetos reales.
Entonces resuelve ese problema, uno puede
Defina un protocolo que defina los métodos para convertir hacia Data
y hacia atrás:
protocol DataConvertible {
init?(data: Data)
var data: Data { get }
}
Implemente las conversiones como métodos predeterminados en una extensión de protocolo:
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) }
}
}
He elegido un inicializador fallable aquí que comprueba que el número de bytes proporcionados coincide con el tamaño del tipo.
Y, finalmente, declare la conformidad con todos los tipos que se pueden convertir de forma segura Data
y viceversa:
extension Int : DataConvertible { }
extension Float : DataConvertible { }
extension Double : DataConvertible { }
// add more types here ...
Esto hace que la conversión sea aún más elegante:
let value = 42.13
let data = value.data
print(data as NSData) // <713d0ad7 a3104540>
if let roundtrip = Double(data: data) {
print(roundtrip) // 42.13
}
La ventaja del segundo enfoque es que no puede realizar conversiones inseguras sin darse cuenta. La desventaja es que debe enumerar todos los tipos "seguros" de forma explícita.
También puede implementar el protocolo para otros tipos que requieren una conversión no trivial, como:
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)
}
}
o implemente los métodos de conversión en sus propios tipos para hacer lo que sea necesario para serializar y deserializar un valor.
Orden de bytes
No se realiza ninguna conversión de orden de bytes en los métodos anteriores, los datos siempre están en el orden de bytes del host. Para una representación independiente de la plataforma (por ejemplo, "big endian" también conocido como orden de bytes de "red"), use las propiedades enteras correspondientes resp. inicializadores. Por ejemplo:
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
}
Por supuesto, esta conversión también se puede realizar de forma general, en el método de conversión genérico.