Hin- und Rückfahrt Schnelle Nummerntypen zu / von Daten

94

Da sich Swift 3 Datastattdessen zu [UInt8]neigt, versuche ich herauszufinden, wie verschiedene Zahlentypen (UInt8, Double, Float, Int64 usw.) als Datenobjekte am effizientesten / idiomatischsten codiert / decodiert werden können.

Es gibt diese Antwort für die Verwendung von [UInt8] , aber es scheint verschiedene Zeiger-APIs zu verwenden, die ich in Data nicht finden kann.

Ich möchte im Grunde einige benutzerdefinierte Erweiterungen, die ungefähr so ​​aussehen:

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

Der Teil, der mir wirklich entgeht, ich habe eine Reihe von Dokumenten durchgesehen, ist, wie ich aus jeder Grundstruktur (die alle Zahlen sind) eine Art Zeigersache (OpaquePointer oder BufferPointer oder UnsafePointer?) Beziehen kann. In C würde ich einfach ein kaufmännisches Und davor schlagen, und los geht's.

Travis Griggs
quelle

Antworten:

256

Hinweis: Der Code wurde jetzt für Swift 5 (Xcode 10.2) aktualisiert . (Die Versionen Swift 3 und Swift 4.2 finden Sie im Bearbeitungsverlauf.) Auch möglicherweise nicht ausgerichtete Daten werden jetzt korrekt behandelt.

Wie man Dataaus einem Wert schafft

Ab Swift 4.2 können Daten aus einem Wert einfach mit erstellt werden

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

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

Erläuterung:

  • withUnsafeBytes(of: value) Ruft den Abschluss mit einem Pufferzeiger auf, der die Rohbytes des Werts abdeckt.
  • Ein Rohpufferzeiger ist eine Folge von Bytes und Data($0)kann daher zum Erstellen der Daten verwendet werden.

So rufen Sie einen Wert ab Data

Ab Swift 5 ruft das withUnsafeBytes(_:)of Dataden Abschluss mit einem "untyped" UnsafeMutableRawBufferPointerfür die Bytes auf. Die load(fromByteOffset:as:)Methode, die den Wert aus dem Speicher liest:

let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes {
    $0.load(as: Double.self)
}
print(value) // 42.13

Es gibt ein Problem mit diesem Ansatz: Es setzt voraus , dass die Speichereigenschaft ist ausgerichtet für den Typen (hier: zu einer 8-Byte - Adresse ausgerichtet). Dies ist jedoch nicht garantiert, z. B. wenn die Daten als Schnitt eines anderen DataWerts erhalten wurden.

Es ist daher sicherer, die Bytes auf den Wert zu kopieren :

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

Erläuterung:

  • withUnsafeMutableBytes(of:_:) Ruft den Abschluss mit einem veränderlichen Pufferzeiger auf, der die Rohbytes des Werts abdeckt.
  • Das copyBytes(to:)Verfahren von DataProtocol(zu dem DataKonform) Kopiert Bytes aus den Daten in diesem Puffer.

Der Rückgabewert von copyBytes()ist die Anzahl der kopierten Bytes. Sie entspricht der Größe des Zielpuffers oder weniger, wenn die Daten nicht genügend Bytes enthalten.

Generische Lösung # 1

Die oben genannten Konvertierungen können jetzt einfach als generische Methoden implementiert werden für 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
    }
}

Die Einschränkung T: ExpressibleByIntegerLiteralwird hier hinzugefügt, damit wir den Wert leicht auf "Null" initialisieren können - das ist keine wirkliche Einschränkung, da diese Methode ohnehin mit "Trival" -Typen (Ganzzahl und Gleitkomma) verwendet werden kann, siehe unten.

Beispiel:

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

Ebenso können Sie Arrays hin Dataund zurück konvertieren :

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

Beispiel:

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]

Generische Lösung # 2

Der obige Ansatz hat einen Nachteil: Er funktioniert tatsächlich nur mit "trivialen" Typen wie Ganzzahlen und Gleitkommatypen. "Komplexe" Typen mögen Array und Stringhaben (versteckte) Zeiger auf den zugrunde liegenden Speicher und können nicht durch einfaches Kopieren der Struktur selbst weitergegeben werden. Es würde auch nicht mit Referenztypen funktionieren, die nur Zeiger auf den realen Objektspeicher sind.

Also kann man dieses Problem lösen, man kann

  • Definieren Sie ein Protokoll, das die Methoden für die Konvertierung nach Dataund zurück definiert:

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
  • Implementieren Sie die Konvertierungen als Standardmethoden in einer Protokollerweiterung:

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

    Ich habe hier einen fehlgeschlagenen Initialisierer ausgewählt, der überprüft, ob die Anzahl der bereitgestellten Bytes mit der Größe des Typs übereinstimmt.

  • Und schließlich die Konformität mit allen Typen erklären, die sicher hin Dataund zurück konvertiert werden können:

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

Dies macht den Umbau noch eleganter:

let value = 42.13
let data = value.data
print(data as NSData) // <713d0ad7 a3104540>

if let roundtrip = Double(data: data) {
    print(roundtrip) // 42.13
}

Der Vorteil des zweiten Ansatzes besteht darin, dass Sie nicht versehentlich unsichere Konvertierungen durchführen können. Der Nachteil ist, dass Sie alle "sicheren" Typen explizit auflisten müssen.

Sie können das Protokoll auch für andere Typen implementieren, für die eine nicht triviale Konvertierung erforderlich ist, z.

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

oder implementieren Sie die Konvertierungsmethoden in Ihren eigenen Typen, um alles Notwendige zu tun, also serialisieren und deserialisieren Sie einen Wert.

Bytereihenfolge

Bei den obigen Methoden wird keine Konvertierung der Bytereihenfolge durchgeführt. Die Daten befinden sich immer in der Reihenfolge der Hostbytes. Verwenden Sie für eine plattformunabhängige Darstellung (z. B. "Big Endian", auch bekannt als "Netzwerk" -Byte-Reihenfolge) die entsprechenden Ganzzahl-Eigenschaften bzw. Initialisierer. Beispielsweise:

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
}

Natürlich kann diese Konvertierung auch allgemein in der generischen Konvertierungsmethode durchgeführt werden.

Martin R.
quelle
Bedeutet die Tatsache, dass wir eine varKopie des Anfangswertes erstellen müssen, dass wir die Bytes zweimal kopieren? In meinem aktuellen Anwendungsfall wandle ich sie in Datenstrukturen um, damit ich appendsie in einen wachsenden Strom von Bytes umwandeln kann . In Straight C ist dies so einfach wie *(cPointer + offset) = originalValue. Die Bytes werden also nur einmal kopiert.
Travis Griggs
1
@TravisGriggs: Das Kopieren eines Int oder Floats ist höchstwahrscheinlich nicht relevant, aber Sie können ähnliche Dinge in Swift tun. Wenn Sie eine haben ptr: UnsafeMutablePointer<UInt8>, können Sie dem referenzierten Speicher etwas zuweisen, UnsafeMutablePointer<T>(ptr + offset).pointee = valuedas genau Ihrem Swift-Code entspricht. Es gibt ein potenzielles Problem: Einige Prozessoren erlauben nur einen ausgerichteten Speicherzugriff, z. B. können Sie ein Int nicht an einem ungeraden Speicherort speichern. Ich weiß nicht, ob dies für die derzeit verwendeten Intel- und ARM-Prozessoren gilt.
Martin R
1
@TravisGriggs: (Fortsetzung) ... Dies setzt auch voraus, dass bereits ein ausreichend großes Datenobjekt erstellt wurde. In Swift können Sie das Datenobjekt nur erstellen und initialisieren , sodass Sie während des Vorgangs möglicherweise eine zusätzliche Kopie von null Byte haben Initialisierung. - Wenn Sie weitere Details benötigen, würde ich vorschlagen, dass Sie eine neue Frage stellen.
Martin R
2
@ HansBrende: Ich fürchte, das ist derzeit nicht möglich. Es würde eine erfordern extension Array: DataConvertible where Element: DataConvertible. Das ist in Swift 3 nicht möglich, aber für Swift 4 geplant (soweit ich weiß). Vergleichen Sie "Bedingte Konformitäten" in github.com/apple/swift/blob/master/docs/…
Martin R
1
@m_katsifarakis: Könnte es sein , dass Sie sich vertippt , Int.selfwie Int.Type?
Martin R
3

Sie können einen unsicheren Zeiger auf veränderbare Objekte erhalten, indem Sie Folgendes verwenden withUnsafePointer:

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

Ich kenne keine Möglichkeit, eine für unveränderliche Objekte zu erhalten, da der Operator inout nur für veränderbare Objekte funktioniert.

Dies wird in der Antwort gezeigt, auf die Sie verlinkt haben.

zneak
quelle
2

In meinem Fall hat die Antwort von Martin R geholfen, aber das Ergebnis wurde umgekehrt. Also habe ich eine kleine Änderung in seinem Code vorgenommen:

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

Das Problem hängt mit LittleEndian und BigEndian zusammen.

Beto Caldas
quelle