w obie strony Szybkie typy liczb do / z danych

95

Ponieważ Swift 3 pochyla się w stronę Datazamiast tego [UInt8], próbuję znaleźć najbardziej wydajny / idiomatyczny sposób kodowania / dekodowania swiftów różnych typów liczb (UInt8, Double, Float, Int64 itp.) Jako obiektów danych.

Jest taka odpowiedź na użycie [UInt8] , ale wydaje się, że używa różnych interfejsów API wskaźników, których nie mogę znaleźć w Data.

Zasadniczo chciałbym mieć niestandardowe rozszerzenia, które wyglądają mniej więcej tak:

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

Część, która naprawdę mi umyka, przejrzałem kilka dokumentów, to sposób, w jaki mogę uzyskać coś w rodzaju wskaźnika (OpaquePointer lub BufferPointer lub UnsafePointer?) Z dowolnej podstawowej struktury (którą są wszystkie liczby). W C po prostu uderzyłbym przed nim znak ampersand i gotowe.

Travis Griggs
źródło

Odpowiedzi:

260

Uwaga: kod został zaktualizowany dla Swift 5 (Xcode 10.2). (Wersje Swift 3 i Swift 4.2 można znaleźć w historii edycji). Również prawdopodobnie niewyrównane dane są teraz prawidłowo obsługiwane.

Jak tworzyć Dataz wartości

Począwszy od wersji Swift 4.2, dane można tworzyć z wartości po prostu za pomocą

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

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

Wyjaśnienie:

  • withUnsafeBytes(of: value) wywołuje zamknięcie ze wskaźnikiem buforu obejmującym nieprzetworzone bajty wartości.
  • Surowy wskaźnik bufora to sekwencja bajtów, dlatego Data($0)może służyć do tworzenia danych.

Jak pobrać wartość z Data

Począwszy od Swift 5, withUnsafeBytes(_:)of Datawywołuje zamknięcie z „untyped” UnsafeMutableRawBufferPointerdo bajtów. load(fromByteOffset:as:)Metoda odczytuje wartość z pamięci:

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

Jest jeden problem z tym podejściem: wymaga, aby pamięć była wyrównana do typu (tutaj: wyrównana do adresu 8-bajtowego). Ale nie jest to gwarantowane, np. Jeśli dane zostały uzyskane jako wycinek innej Datawartości.

Dlatego bezpieczniej jest skopiować bajty do wartości:

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

Wyjaśnienie:

  • withUnsafeMutableBytes(of:_:) wywołuje zamknięcie ze zmiennym wskaźnikiem buforu obejmującym nieprzetworzone bajty wartości.
  • copyBytes(to:)Sposób DataProtocol(na który Datajest zgodny z bajtów) kopiuje się dane w tym buforze.

Wartość zwracana copyBytes()to liczba skopiowanych bajtów. Jest równy rozmiarowi buforu docelowego lub mniejszy, jeśli dane nie zawierają wystarczającej liczby bajtów.

Ogólne rozwiązanie nr 1

Powyższe konwersje można teraz łatwo zaimplementować jako ogólne metody 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
    }
}

Ograniczenie T: ExpressibleByIntegerLiteraljest tutaj dodawane, abyśmy mogli łatwo zainicjować wartość na „zero” - nie jest to tak naprawdę ograniczenie, ponieważ ta metoda i tak może być używana z typami „trival” (liczby całkowite i zmiennoprzecinkowe), patrz poniżej.

Przykład:

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

Podobnie możesz konwertować tablice na Dataiz powrotem:

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

Przykład:

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]

Ogólne rozwiązanie nr 2

Powyższe podejście ma jedną wadę: w rzeczywistości działa tylko z "trywialnymi" typami, takimi jak liczby całkowite i typy zmiennoprzecinkowe. Typy „złożone”, takie jak Array i Stringmają (ukryte) wskaźniki do bazowego magazynu i nie mogą być przekazywane przez zwykłe skopiowanie samej struktury. Nie działałoby również z typami referencyjnymi, które są tylko wskaźnikami do rzeczywistej pamięci obiektów.

Więc można rozwiązać ten problem

  • Zdefiniuj protokół, który definiuje metody konwersji do Dataiz powrotem:

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
    
  • Zaimplementuj konwersje jako metody domyślne w rozszerzeniu protokołu:

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

    Wybrałem tutaj dostępny inicjalizator, który sprawdza, czy liczba podanych bajtów odpowiada rozmiarowi typu.

  • Na koniec zadeklaruj zgodność ze wszystkimi typami, które można bezpiecznie konwertować na Dataiz powrotem:

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

To sprawia, że ​​konwersja jest jeszcze bardziej elegancka:

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

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

Zaletą drugiego podejścia jest to, że nie można przypadkowo wykonać niebezpiecznych konwersji. Wadą jest to, że musisz jawnie wymienić wszystkie „bezpieczne” typy.

Możesz również zaimplementować protokół dla innych typów, które wymagają nietrywialnej konwersji, takich jak:

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

lub zaimplementuj metody konwersji we własnych typach, aby zrobić wszystko, co jest konieczne, aby serializować i deserializować wartość.

Kolejność bajtów

W powyższych metodach nie jest wykonywana żadna konwersja kolejności bajtów, dane są zawsze w kolejności bajtów hosta. Aby uzyskać reprezentację niezależną od platformy (np. „Big endian” lub kolejność bajtów „sieć”), użyj odpowiednich właściwości liczb całkowitych, odpowiednio. inicjatory. Na przykład:

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
}

Oczywiście tę konwersję można również przeprowadzić ogólnie, w ogólnej metodzie konwersji.

Martin R.
źródło
Czy fakt, że musimy wykonać varkopię wartości początkowej, oznacza, że ​​kopiujemy bajty dwukrotnie? W moim obecnym przypadku zamieniam je w struktury danych, więc mogę appendje przekształcić w rosnący strumień bajtów. W prostym C jest to tak proste, jak *(cPointer + offset) = originalValue. Więc bajty są kopiowane tylko raz.
Travis Griggs
1
@TravisGriggs: Kopiowanie int lub float nie będzie najprawdopodobniej istotne, ale może robić podobne rzeczy w Swift. Jeśli masz ptr: UnsafeMutablePointer<UInt8>, możesz przypisać do przywoływanej pamięci za pomocą czegoś podobnego, UnsafeMutablePointer<T>(ptr + offset).pointee = valueco ściśle odpowiada Twojemu kodowi Swift. Jest jeden potencjalny problem: niektóre procesory pozwalają tylko na wyrównany dostęp do pamięci, np. Nie można zapisać Int w dziwnym miejscu w pamięci. Nie wiem, czy dotyczy to obecnie używanych procesorów Intel i ARM.
Martin R
1
@TravisGriggs: (cd.) ... Wymaga to również, aby wystarczająco duży obiekt Data został już utworzony, aw Swift możesz tylko utworzyć i zainicjować obiekt Data, więc możesz mieć dodatkową kopię zerowych bajtów podczas inicjalizacja. - Jeśli potrzebujesz więcej informacji, proponuję zadać nowe pytanie.
Martin R
2
@HansBrende: Obawiam się, że obecnie nie jest to możliwe. Wymagałoby to extension Array: DataConvertible where Element: DataConvertible. Nie jest to możliwe w Swift 3, ale planowane dla Swift 4 (o ile wiem). Porównaj „Warunki warunkowe” na github.com/apple/swift/blob/master/docs/…
Martin R
1
@m_katsifarakis: Czy to możliwe, że wpisałeś Int.selfjako Int.Type?
Martin R
3

Możesz uzyskać niebezpieczny wskaźnik do zmiennych obiektów, używając withUnsafePointer:

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

Nie znam sposobu, aby uzyskać taki dla obiektów niezmiennych, ponieważ operator inout działa tylko na obiektach zmiennych.

Jest to pokazane w odpowiedzi, z którą się łączysz.

zneak
źródło
2

W moim przypadku odpowiedź Martina R pomogła, ale wynik był odwrotny. Więc zrobiłem małą zmianę w jego kodzie:

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

Problem jest związany z LittleEndian i BigEndian.

Beto Caldas
źródło