web-dev-qa-db-fra.com

aller-retour Swift types de nombres à / de Données

Avec Swift 3 penché vers Data au lieu de [UInt8], j'essaie de trouver la manière la plus efficace/idiomatique d'encoder/décoder des types différents de nombres (UInt8, Double , Float, Int64, etc.) en tant qu’objets de données.

Il y a cette réponse pour utiliser [UInt8] , mais il semble utiliser diverses API de pointeur que je ne trouve pas dans Data.

Je voudrais fondamentalement des extensions personnalisées qui ressemblent à quelque chose comme:

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

La partie qui m’échappe vraiment, j’ai parcouru un tas de documents, est de savoir comment obtenir un pointeur (OpaquePointer ou BufferPointer ou UnsafePointer?) À partir de n’importe quelle structure de base (c’est-à-dire tous les nombres). En C, je me contenterais de taper une esperluette devant elle et le tour est joué.

83
Travis Griggs

Remarque: Le code a été mis à jour pour Swift 5 (Xcode 10.2) maintenant. (Les versions de Swift 3 et Swift 4.2 sont disponibles dans l'historique des modifications.) De plus, les données éventuellement non alignées sont maintenant correctement traitées.

Comment créer Data à partir d'une valeur

À partir de 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) appelle la fermeture avec un pointeur de mémoire tampon couvrant les octets bruts de la valeur.
  • Un pointeur de tampon brut est une séquence d'octets. Par conséquent, Data($0) peut être utilisé pour créer les données.

Comment récupérer une valeur de Data

À partir de Swift 5, le withUnsafeBytes(_:) de Data appelle la fermeture avec un _ sans type UnsafeMutableRawBufferPointer aux octets. La méthode load(fromByteOffset:as:) lit la valeur dans 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 existe un problème avec cette approche: elle nécessite que la mémoire soit une propriété alignée pour le type (ici: alignée sur une adresse à 8 octets). Mais cela n’est pas garanti, par exemple. si les données ont été obtenues sous forme de tranche d'une autre valeur Data.

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:_:) appelle la fermeture avec un pointeur de tampon mutable couvrant les octets bruts de la valeur.
  • La méthode copyBytes(to:) de DataProtocol (à laquelle Data se conforme) copie les octets des données dans 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 inférieur si les données ne contiennent pas suffisamment d'octets.

Solution générique n ° 1

Les conversions ci-dessus peuvent maintenant ê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 afin que nous puissions facilement initialiser la valeur à "zéro" - ce n'est pas vraiment une restriction car cette méthode peut ê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 n ° 2

L'approche ci-dessus présente un inconvénient: elle ne fonctionne en fait qu'avec des types "triviaux" tels que les entiers et les types à virgule flottante. Les types "complexes" tels que Array et String ont des pointeurs (masqués) sur la mémoire de stockage sous-jacente et ne peuvent pas être contournés en copiant simplement la structure elle-même. Cela ne fonctionnerait pas non plus avec les types de référence qui ne sont que des pointeurs sur le stockage d'objets réel.

Alors résolvez ce problème, on peut

  • Définissez un protocole définissant les méthodes de conversion en 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) }
        }
    }
    

    J'ai choisi un initialiseur disponible ici qui vérifie que le nombre d'octets fournis correspond à la taille du type.

  • Et enfin, déclarez la conformité à tous les types pouvant être convertis en toute sécurité en Data et inversement:

    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 seconde approche est que vous ne pouvez pas faire par inadvertance des conversions dangereuses. 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 nécessitant 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 d'octet n'est effectuée dans les méthodes ci-dessus, les données sont toujours dans l'ordre d'octet hôte. Pour une représentation indépendante de la plate-forme (par exemple, "big endian" ou "ordre des octets du réseau"), utilisez les propriétés entières correspondantes, respectivement. 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 être effectuée de manière générale, dans la méthode de conversion générique.

213
Martin R

Vous pouvez obtenir un pointeur non sécurisé sur des objets mutables en utilisant withUnsafePointer :

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

Je ne sais pas comment en obtenir un pour les objets immuables, car l'opérateur inout ne fonctionne que sur des objets mutables.

Ceci est démontré dans la réponse que vous avez liée à.

3
zneak

Dans mon cas, la réponse de Martin R a aidé, mais le résultat a été inversé. Alors j'ai fait un petit changement dans son code:

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

Le problème est lié à LittleEndian et BigEndian.

2
Beto Caldas