Remarque: le code a été mis à jour pour Swift 5 (Xcode 10.2) maintenant. (Les versions Swift 3 et Swift 4.2 peuvent être trouvées dans l'historique des modifications.) Les données éventuellement non alignées sont désormais correctement gérées.
Comment créer à Data
partir d'une valeur
Depuis Swift 4.2, les données peuvent être créées à partir d'une valeur simplement avec
let value = 42.13
let data = withUnsafeBytes(of: value) { Data($0) }
print(data as NSData) // <713d0ad7 a3104540>
Explication:
withUnsafeBytes(of: value)
invoque la fermeture avec un pointeur de tampon couvrant les octets bruts de la valeur.
- Un pointeur de tampon brut est une séquence d'octets et
Data($0)
peut donc être utilisé pour créer les données.
Comment récupérer une valeur de Data
Depuis Swift 5, le withUnsafeBytes(_:)
of Data
appelle la fermeture avec un «non typé» UnsafeMutableRawBufferPointer
aux octets. La load(fromByteOffset:as:)
méthode qui lit la valeur de la mémoire:
let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes {
$0.load(as: Double.self)
}
print(value) // 42.13
Il y a un problème avec cette approche: elle nécessite que la mémoire soit alignée sur la propriété pour le type (ici: alignée sur une adresse de 8 octets). Mais cela n'est pas garanti, par exemple si les données ont été obtenues sous forme de tranche d'une autre Data
valeur.
Il est donc plus sûr de copier les octets dans la valeur:
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
Explication:
withUnsafeMutableBytes(of:_:)
invoque la fermeture avec un pointeur de tampon mutable couvrant les octets bruts de la valeur.
- La
copyBytes(to:)
méthode de DataProtocol
(à laquelle se Data
conforme) copie les octets des données vers ce tampon.
La valeur de retour de copyBytes()
est le nombre d'octets copiés. Il est égal à la taille du tampon de destination, ou moins si les données ne contiennent pas suffisamment d'octets.
Solution générique n ° 1
Les conversions ci-dessus peuvent désormais être facilement implémentées en tant que méthodes génériques 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 contrainte T: ExpressibleByIntegerLiteral
est ajoutée ici pour que nous puissions facilement initialiser la valeur à «zéro» - ce n'est pas vraiment une restriction car cette méthode peut quand même être utilisée avec les types «trival» (entier et virgule flottante), voir ci-dessous.
Exemple:
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")
}
De même, vous pouvez convertir des tableaux en Data
et inversement:
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
}
}
Exemple:
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]
Solution générique # 2
L'approche ci-dessus a un inconvénient: elle ne fonctionne en fait qu'avec des types "triviaux" comme les entiers et les types à virgule flottante. Types "complexes" comme Array
etString
ont des pointeurs (cachés) vers le stockage sous-jacent et ne peuvent pas être transmis en copiant simplement la structure elle-même. Cela ne fonctionnerait pas non plus avec des types de référence qui ne sont que des pointeurs vers le stockage d'objets réel.
Alors résolvez ce problème, on peut
Définissez un protocole qui définit les méthodes de conversion vers Data
et inversement:
protocol DataConvertible {
init?(data: Data)
var data: Data { get }
}
Implémentez les conversions comme méthodes par défaut dans une extension de protocole:
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) }
}
}
Je l' ai choisi un failable initialiseur ici qui vérifie que le nombre d'octets fournis correspond à la taille du type.
Et enfin, déclarez la conformité à tous les types qui peuvent être convertis Data
et inversés en toute sécurité :
extension Int : DataConvertible { }
extension Float : DataConvertible { }
extension Double : DataConvertible { }
// add more types here ...
Cela rend la conversion encore plus élégante:
let value = 42.13
let data = value.data
print(data as NSData) // <713d0ad7 a3104540>
if let roundtrip = Double(data: data) {
print(roundtrip) // 42.13
}
L'avantage de la deuxième approche est que vous ne pouvez pas effectuer par inadvertance des conversions non sécurisées. L'inconvénient est que vous devez lister explicitement tous les types "sûrs".
Vous pouvez également implémenter le protocole pour d'autres types qui nécessitent une conversion non triviale, tels que:
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)
}
}
ou implémentez les méthodes de conversion dans vos propres types pour faire tout ce qui est nécessaire pour sérialiser et désérialiser une valeur.
Ordre des octets
Aucune conversion d'ordre des octets n'est effectuée dans les méthodes ci-dessus, les données sont toujours dans l'ordre des octets de l'hôte. Pour une représentation indépendante de la plate-forme (par exemple «big endian» aka «network» byte order), utilisez les propriétés entières correspondantes resp. initialiseurs. Par exemple:
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
}
Bien entendu, cette conversion peut également se faire de manière générale, dans la méthode de conversion générique.