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 Data
da 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 Data
richiama la chiusura con un "untyped" UnsafeMutableRawBufferPointer
ai 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 Data
valore.
È 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 è Data
conforme) 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: ExpressibleByIntegerLiteral
viene 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 Data
e 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 String
hanno 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 Data
e 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 Data
e 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.