Round trip Swift tipi di numeri da / per i dati


94

Con Swift 3 che si appoggia Datainvece di [UInt8], sto cercando di scoprire quale sia il modo più efficiente / idiomatico per codificare / decodificare swift vari tipi di numeri (UInt8, Double, Float, Int64, ecc.) Come oggetti dati.

C'è questa risposta per l'utilizzo di [UInt8] , ma sembra che stia utilizzando varie API di puntatore che non riesco a trovare su Data.

Vorrei fondamentalmente alcune estensioni personalizzate che assomigliano a qualcosa di simile:

let input = 42.13 // implicit Double
let bytes = input.data
let roundtrip = bytes.to(Double) // --> 42.13

La parte che mi sfugge davvero, ho esaminato un sacco di documenti, è come posso ottenere una sorta di puntatore (OpaquePointer o BufferPointer o UnsafePointer?) Da qualsiasi struttura di base (quali sono tutti i numeri). In C, vorrei solo schiaffeggiare una e commerciale di fronte ad essa, e ci sei.


Risposte:


256

Nota: il codice è stato aggiornato per Swift 5 (Xcode 10.2) ora. (Le versioni Swift 3 e Swift 4.2 possono essere trovate nella cronologia delle modifiche.) Anche i dati eventualmente non allineati ora vengono gestiti correttamente.

Come creare Datada un valore

A partire da Swift 4.2, i dati possono essere creati da un valore semplicemente con

let value = 42.13
let data = withUnsafeBytes(of: value) { Data($0) }

print(data as NSData) // <713d0ad7 a3104540>

Spiegazione:

  • withUnsafeBytes(of: value) invoca la chiusura con un puntatore del buffer che copre i byte grezzi del valore.
  • Un puntatore a buffer non elaborato è una sequenza di byte, pertanto Data($0)può essere utilizzato per creare i dati.

Come recuperare un valore da Data

A partire da Swift 5, il withUnsafeBytes(_:)of Datarichiama la chiusura con un "untyped" UnsafeMutableRawBufferPointerai byte. Il load(fromByteOffset:as:)metodo legge il valore dalla 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

C'è un problema con questo approccio: richiede che la memoria sia allineata alla proprietà per il tipo (qui: allineata a un indirizzo a 8 byte). Ma ciò non è garantito, ad esempio se i dati sono stati ottenuti come uno slice di un altro Datavalore.

È quindi più sicuro copiare i byte nel valore:

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

Spiegazione:

  • withUnsafeMutableBytes(of:_:) invoca la chiusura con un puntatore a buffer mutabile che copre i byte grezzi del valore.
  • Il copyBytes(to:)metodo di DataProtocol(a cui è Dataconforme) copia i byte dai dati a quel buffer.

Il valore restituito di copyBytes()è il numero di byte copiati. È uguale alla dimensione del buffer di destinazione o inferiore se i dati non contengono byte sufficienti.

Soluzione generica n. 1

Le conversioni di cui sopra possono ora essere facilmente implementate come metodi generici di 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
    }
}

Il vincolo T: ExpressibleByIntegerLiteralviene aggiunto qui in modo che possiamo facilmente inizializzare il valore su "zero" - questa non è realmente una restrizione perché questo metodo può essere utilizzato comunque con i tipi "trival" (intero e virgola mobile), vedi sotto.

Esempio:

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")
}

Allo stesso modo, puoi convertire gli array in Datae indietro:

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
    }
}

Esempio:

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]

Soluzione generica n. 2

L'approccio precedente ha uno svantaggio: in realtà funziona solo con tipi "banali" come numeri interi e tipi a virgola mobile. I tipi "complessi" come Array e Stringhanno puntatori (nascosti) alla memoria sottostante e non possono essere passati semplicemente copiando la struttura stessa. Inoltre non funzionerebbe con i tipi di riferimento che sono solo puntatori all'archiviazione di oggetti reali.

Quindi risolvi il problema, puoi

  • Definire un protocollo che definisca i metodi per la conversione Datae viceversa:

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
  • Implementa le conversioni come metodi predefiniti in un'estensione di protocollo:

    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) }
        }
    }

    Ho scelto un inizializzatore non riuscito qui che controlla che il numero di byte fornito corrisponda alla dimensione del tipo.

  • E infine dichiara la conformità a tutti i tipi che possono essere convertiti in sicurezza Datae viceversa:

    extension Int : DataConvertible { }
    extension Float : DataConvertible { }
    extension Double : DataConvertible { }
    // add more types here ...

Questo rende la conversione ancora più 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
}

Il vantaggio del secondo approccio è che non è possibile eseguire inavvertitamente conversioni non sicure. Lo svantaggio è che devi elencare esplicitamente tutti i tipi "sicuri".

Potresti anche implementare il protocollo per altri tipi che richiedono una conversione non banale, come:

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)
    }
}

oppure implementa i metodi di conversione nei tuoi tipi per fare tutto ciò che è necessario, quindi serializza e deserializza un valore.

Ordine byte

Nessuna conversione dell'ordine dei byte viene eseguita nei metodi precedenti, i dati sono sempre nell'ordine dei byte dell'host. Per una rappresentazione indipendente dalla piattaforma (ad esempio "big endian" aka "network" byte order), utilizzare le corrispondenti proprietà integer risp. inizializzatori. Per esempio:

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
}

Ovviamente questa conversione può essere eseguita anche in generale, nel metodo di conversione generico.


Il fatto che dobbiamo fare una varcopia del valore iniziale, significa che stiamo copiando i byte due volte? Nel mio attuale caso d'uso, li sto trasformando in strutture dati, quindi posso trasformarli in appendun flusso crescente di byte. In C dritto, questo è facile come *(cPointer + offset) = originalValue. Quindi i byte vengono copiati solo una volta.
Travis Griggs

1
@TravisGriggs: la copia di un int o di un float molto probabilmente non sarà rilevante, ma puoi fare cose simili in Swift. Se hai un ptr: UnsafeMutablePointer<UInt8>, puoi assegnarlo alla memoria di riferimento tramite qualcosa di simile UnsafeMutablePointer<T>(ptr + offset).pointee = valueche corrisponde strettamente al tuo codice Swift. C'è un potenziale problema: alcuni processori consentono solo l'accesso alla memoria allineato , ad esempio non è possibile memorizzare un Int in una posizione di memoria dispari. Non so se ciò si applica ai processori Intel e ARM attualmente utilizzati.
Martin R

1
@TravisGriggs: (continua) ... Anche questo richiede che sia già stato creato un oggetto Data sufficientemente grande, e in Swift puoi solo creare e inizializzare l'oggetto Data, quindi potresti avere una copia aggiuntiva di zero byte durante il inizializzazione. - Se hai bisogno di maggiori dettagli, ti suggerisco di pubblicare una nuova domanda.
Martin R

2
@ HansBrende: Temo che al momento non sia possibile. Richiederebbe un file extension Array: DataConvertible where Element: DataConvertible. Ciò non è possibile in Swift 3, ma previsto per Swift 4 (per quanto ne so). Confronta "Conformità condizionali" in github.com/apple/swift/blob/master/docs/…
Martin R

1
@m_katsifarakis: Potrebbe essere che hai digitato male Int.selfcome Int.Type?
Martin R

3

È possibile ottenere un puntatore pericoloso mutevoli oggetti utilizzando withUnsafePointer:

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

Non conosco un modo per ottenerne uno per oggetti immutabili, perché l'operatore inout funziona solo su oggetti mutabili.

Questo è dimostrato nella risposta a cui ti sei collegato.


2

Nel mio caso, la risposta di Martin R ha aiutato ma il risultato è stato invertito. Quindi ho fatto una piccola modifica nel suo codice:

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))
    }
}

Il problema è legato a LittleEndian e BigEndian.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.