ida y vuelta tipos de números Swift hacia / desde datos

95

Con Swift 3 inclinándose hacia en Datalugar 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.

Travis Griggs
fuente

Respuestas:

259

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 Datapartir 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 Datainvoca el cierre con un "sin tipo" UnsafeMutableRawBufferPointeren 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 Datavalor.

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 Dataajusta) 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: ExpressibleByIntegerLiteralse 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 Dataida 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 Datay 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 Datay 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.

Martín R
fuente
¿El hecho de que tengamos que hacer una varcopia 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 puedo appendconvertirlos 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.
Travis Griggs
1
@TravisGriggs: Lo más probable es que copiar un int o float no sea relevante, pero puede hacer cosas similares en Swift. Si tiene un ptr: UnsafeMutablePointer<UInt8>, puede asignarlo a la memoria referenciada a través de algo parecido a lo UnsafeMutablePointer<T>(ptr + offset).pointee = valueque 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.
Martin R
1
@TravisGriggs: (continuación) ... Además, esto requiere que ya se haya creado un objeto de datos suficientemente grande, y en Swift solo puede crear e inicializar el objeto de datos, por lo que es posible que tenga una copia adicional de cero bytes durante el inicialización. - Si necesita más detalles, le sugiero que publique una nueva pregunta.
Martin R
2
@HansBrende: Me temo que eso no es posible actualmente. Requeriría un extension Array: DataConvertible where Element: DataConvertible. Eso no es posible en Swift 3, pero está planeado para Swift 4 (hasta donde yo sé). Comparar " Conformidades
Martin R
1
@m_katsifarakis: ¿Podría ser que escribiste mal Int.selfcomo Int.Type?
Martin R
3

Puede obtener un puntero inseguro a objetos mutables mediante withUnsafePointer:

withUnsafePointer(&input) { /* $0 is your pointer */ }

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.

zneak
fuente
2

En mi caso, la respuesta de Martin R ayudó, pero el resultado se invirtió. Entonces hice un pequeño cambio en su código:

extension UInt16 : DataConvertible {

    init?(data: Data) {
        guard data.count == MemoryLayout<UInt16>.size else { 
          return nil 
        }
    self = data.withUnsafeBytes { $0.pointee }
    }

    var data: Data {
         var value = CFSwapInt16HostToBig(self)//Acho que o padrao do IOS 'e LittleEndian, pois os bytes estavao ao contrario
         return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }
}

El problema está relacionado con LittleEndian y BigEndian.

Beto Caldas
fuente