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ś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)
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 Data
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)
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)
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 Data
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
let data = Data(from: value)
print(data as NSData)
if let roundtrip = data.to(type: Double.self) {
print(roundtrip)
} 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)
let roundtrip = data.toArray(type: Int16.self)
print(roundtrip)
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 String
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 { }
To sprawia, że konwersja jest jeszcze bardziej elegancka:
let value = 42.13
let data = value.data
print(data as NSData)
if let roundtrip = Double(data: data) {
print(roundtrip)
}
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 {
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)
if let roundtrip = Int(data: data) {
print(Int(bigEndian: roundtrip))
}
Oczywiście tę konwersję można również przeprowadzić ogólnie, w ogólnej metodzie konwersji.