types de nombres Swift aller-retour vers / depuis les données

95

Avec Swift 3 penché vers Dataplutôt que vers [UInt8], j'essaie de découvrir quelle est la manière la plus efficace / idiomatique d'encoder / décoder divers types 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 sur Data.

J'aimerais essentiellement 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 regardé à travers un tas de documents, est de savoir comment je peux obtenir une sorte de pointeur (OpaquePointer ou BufferPointer ou UnsafePointer?) À partir de n'importe quelle structure de base (dont tous les nombres sont). En C, je giflerais juste une esperluette devant elle, et voilà.

Travis Griggs
la source

Réponses:

258

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 à Datapartir 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 Dataappelle la fermeture avec un «non typé» UnsafeMutableRawBufferPointeraux 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 Datavaleur.

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 Dataconforme) 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: ExpressibleByIntegerLiteralest 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 Dataet 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 Dataet 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 Dataet 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.

Martin R
la source
Le fait que nous devions faire une varcopie de la valeur initiale signifie-t-il que nous copions les octets deux fois? Dans mon cas d'utilisation actuel, je les transforme en structures de données, afin que je puisse appendles transformer en un flux croissant d'octets. En C droit, c'est aussi simple que *(cPointer + offset) = originalValue. Ainsi, les octets ne sont copiés qu'une seule fois.
Travis Griggs
1
@TravisGriggs: La copie d'un int ou d'un float ne sera probablement pas pertinente, mais vous pouvez faire des choses similaires dans Swift. Si vous avez un, ptr: UnsafeMutablePointer<UInt8>vous pouvez l'assigner à la mémoire référencée via quelque chose comme UnsafeMutablePointer<T>(ptr + offset).pointee = valuequi correspond étroitement à votre code Swift. Il existe un problème potentiel: certains processeurs n'autorisent qu'un accès mémoire aligné , par exemple, vous ne pouvez pas stocker un Int à un emplacement de mémoire impair. Je ne sais pas si cela s'applique aux processeurs Intel et ARM actuellement utilisés.
Martin R
1
@TravisGriggs: (suite) ... Cela nécessite également qu'un objet Data suffisamment grand ait déjà été créé, et dans Swift vous ne pouvez créer et initialiser que l'objet Data, vous pouvez donc avoir une copie supplémentaire de zéro octet pendant le initialisation. - Si vous avez besoin de plus de détails, je vous suggère de poster une nouvelle question.
Martin R
2
@HansBrende: J'ai bien peur que ce ne soit pas possible actuellement. Il faudrait un fichier extension Array: DataConvertible where Element: DataConvertible. Ce n'est pas possible dans Swift 3, mais prévu pour Swift 4 (pour autant que je sache). Comparez les "conformités conditionnelles" sur github.com/apple/swift/blob/master/docs/…
Martin R
1
@m_katsifarakis: Serait - ce que vous avez mal saisi Int.selfcomme Int.Type?
Martin R
3

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

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

Je ne connais pas de moyen d'en obtenir un pour les objets immuables, car l'opérateur inout ne fonctionne que sur les objets mutables.

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

zneak
la source
2

Dans mon cas, la réponse de Martin R a aidé mais le résultat a été inversé. J'ai donc 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.

Beto Caldas
la source