Ponieważ Swift 3 pochyla się w stronę Data
zamiast 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.
swift
swift3
swift-data
Travis Griggs
źródło
źródło
Odpowiedzi:
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ć
Data
z wartościPoczą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.Data($0)
może służyć do tworzenia danych.Jak pobrać wartość z
Data
Począwszy od Swift 5,
withUnsafeBytes(_:)
ofData
wywołuje zamknięcie z „untyped”UnsafeMutableRawBufferPointer
do 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
Data
wartoś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óbDataProtocol
(na któryData
jest 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: ExpressibleByIntegerLiteral
jest 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
Data
iz 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
iString
mają (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
Data
iz 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
Data
iz 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.
źródło
var
kopię wartości początkowej, oznacza, że kopiujemy bajty dwukrotnie? W moim obecnym przypadku zamieniam je w struktury danych, więc mogęappend
je 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.ptr: UnsafeMutablePointer<UInt8>
, możesz przypisać do przywoływanej pamięci za pomocą czegoś podobnego,UnsafeMutablePointer<T>(ptr + offset).pointee = value
co ś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.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/…Int.self
jakoInt.Type
?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.
źródło
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.
źródło