Con Swift 3 inclinándose hacia en Data
lugar de [UInt8]
, estoy tratando de descubrir cuál es la forma más eficiente / idiomática de codificar / decodificar swift varios tipos de números (UInt8, Double, Float, Int64, etc.) como objetos de datos.
Existe esta respuesta para usar [UInt8] , pero parece estar usando varias API de puntero que no puedo encontrar en Data.
Me gustaría básicamente algunas extensiones personalizadas que se parecen a:
let input = 42.13 // implicit Double
let bytes = input.data
let roundtrip = bytes.to(Double) // --> 42.13
La parte que realmente se me escapa, he revisado un montón de documentos, es cómo puedo obtener algún tipo de puntero (¿OpaquePointer o BufferPointer o UnsafePointer?) De cualquier estructura básica (que son todos los números). En C, simplemente ponía un signo comercial delante de él, y listo.
swift
swift3
swift-data
Travis Griggs
fuente
fuente
Respuestas:
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 valorA partir de Swift 4.2, los datos se pueden crear a partir de un valor simplemente con
Explicación:
withUnsafeBytes(of: value)
invoca el cierre con un puntero de búfer que cubre los bytes sin procesar del valor.Data($0)
se puede utilizar para crear los datos.Cómo recuperar un valor de
Data
A partir de Swift 5,
withUnsafeBytes(_:)
deData
invoca el cierre con un "sin tipo"UnsafeMutableRawBufferPointer
en los bytes. Elload(fromByteOffset:as:)
método lee el valor de la memoria: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:
Explicación:
withUnsafeMutableBytes(of:_:)
invoca el cierre con un puntero de búfer mutable que cubre los bytes sin procesar del valor.copyBytes(to:)
método deDataProtocol
(con el que seData
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
: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:
Del mismo modo, puede convertir matrices de
Data
ida y vuelta:Ejemplo:
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:Implemente las conversiones como métodos predeterminados en una extensión de protocolo:
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:Esto hace que la conversión sea aún más elegante:
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:
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:
Por supuesto, esta conversión también se puede realizar de forma general, en el método de conversión genérico.
fuente
var
copia del valor inicial significa que estamos copiando los bytes dos veces? En mi caso de uso actual, los estoy convirtiendo en estructuras de datos, por lo que puedoappend
convertirlos en un flujo creciente de bytes. En C recta, esto es tan fácil como*(cPointer + offset) = originalValue
. Entonces, los bytes se copian solo una vez.ptr: UnsafeMutablePointer<UInt8>
, puede asignarlo a la memoria referenciada a través de algo parecido a loUnsafeMutablePointer<T>(ptr + offset).pointee = value
que se corresponda estrechamente con su código Swift. Existe un problema potencial: algunos procesadores solo permiten el acceso a la memoria alineada , por ejemplo, no se puede almacenar un Int en una ubicación de memoria impar. No sé si eso se aplica a los procesadores Intel y ARM que se utilizan actualmente.extension Array: DataConvertible where Element: DataConvertible
. Eso no es posible en Swift 3, pero está planeado para Swift 4 (hasta donde yo sé). Comparar " ConformidadesInt.self
comoInt.Type
?Puede obtener un puntero inseguro a objetos mutables mediante
withUnsafePointer
:No conozco una forma de obtener uno para objetos inmutables, porque el operador inout solo funciona en objetos mutables.
Esto se demuestra en la respuesta a la que ha vinculado.
fuente
En mi caso, la respuesta de Martin R ayudó, pero el resultado se invirtió. Entonces hice un pequeño cambio en su código:
El problema está relacionado con LittleEndian y BigEndian.
fuente