Примітка: Код оновлено для Swift 5 (Xcode 10.2). (Версії Swift 3 та Swift 4.2 можна знайти в історії редагувань.) Також, можливо, незрівняні дані тепер обробляються правильно.
Як створити Data
зі значення
Станом на Swift 4.2, дані можна створювати із значення просто
let value = 42.13
let data = withUnsafeBytes(of: value) { Data($0) }
print(data as NSData) // <713d0ad7 a3104540>
Пояснення:
withUnsafeBytes(of: value)
викликає закриття за допомогою покажчика буфера, що охоплює необроблені байти значення.
- Вказівник необробленого буфера є послідовністю байтів, тому
Data($0)
може використовуватися для створення даних.
Як отримати значення з Data
Станом на Swift 5, withUnsafeBytes(_:)
of Data
викликає закриття з "нетипізованими" UnsafeMutableRawBufferPointer
байтами. load(fromByteOffset:as:)
Метод зчитує значення з пам'яті:
let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes {
$0.load(as: Double.self)
}
print(value) // 42.13
Існує одна проблема з таким підходом: він вимагає, щоб пам’ять була вирівняна за типом властивості (тут: вирівняна до 8-байтової адреси). Але це не гарантується, наприклад, якщо дані були отримані як фрагмент іншогоData
значення.
Тому безпечніше скопіювати байти у значення:
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
Пояснення:
withUnsafeMutableBytes(of:_:)
викликає закриття за допомогою змінного вказівника буфера, що охоплює необроблені байти значення.
copyBytes(to:)
Метод DataProtocol
(до якого Data
відповідає) копіює байти з даних в цьому буфері.
Повернене значення copyBytes()
- це кількість скопійованих байтів. Він дорівнює розміру цільового буфера або менше, якщо дані не містять достатньої кількості байтів.
Загальне рішення No1
Наведені вище перетворення тепер легко реалізувати як загальні методи 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
}
}
T: ExpressibleByIntegerLiteral
Сюди додано обмеження , щоб ми могли легко ініціалізувати значення "нуль" - це насправді не є обмеженням, оскільки цей метод у будь-якому випадку може використовуватися з типами "тривал" (ціле число та плаваюча крапка), див. Нижче.
Приклад:
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")
}
Аналогічним чином ви можете конвертувати масиви в Data
і назад:
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
}
}
Приклад:
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]
Загальне рішення No2
Вищезазначений підхід має один недолік: він насправді працює лише з "тривіальними" типами, такими як цілі числа та типи з плаваючою комою. "Складні" типи типу Array
іString
мають (приховані) вказівники на основне сховище, і їх не можна передавати, просто копіюючи саму структуру. Він також не буде працювати з посилальними типами, які є лише вказівниками на реальне сховище об'єктів.
Тож вирішити цю проблему можна
Визначте протокол, який визначає методи перетворення в Data
і назад:
protocol DataConvertible {
init?(data: Data)
var data: Data { get }
}
Реалізуйте перетворення як методи за замовчуванням у розширенні протоколу:
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) }
}
}
Я вибрав тут недоступний ініціалізатор, який перевіряє, чи надана кількість байтів відповідає розміру типу.
І нарешті, заявіть про відповідність усім типам, які можна безпечно перетворити в Data
і назад:
extension Int : DataConvertible { }
extension Float : DataConvertible { }
extension Double : DataConvertible { }
// add more types here ...
Це робить перетворення ще більш елегантним:
let value = 42.13
let data = value.data
print(data as NSData) // <713d0ad7 a3104540>
if let roundtrip = Double(data: data) {
print(roundtrip) // 42.13
}
Перевага другого підходу полягає в тому, що мимоволі не можна робити небезпечні перетворення. Недоліком є те, що вам доводиться чітко перераховувати всі «безпечні» типи.
Ви також можете реалізувати протокол для інших типів, які вимагають нетривіального перетворення, таких як:
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)
}
}
або застосуйте методи перетворення у власних типах, щоб зробити все необхідне, щоб серіалізувати та десеріалізувати значення.
Порядок байтів
У вищезазначених методах не відбувається перетворення порядку байтів, дані завжди в порядку байтів хоста. Для незалежного від платформи подання (наприклад, “великий ендіан”, він же “мережевий” порядок байтів), використовуйте відповідні цілочисельні властивості, відповідно. ініціалізатори. Наприклад:
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
}
Звичайно, це перетворення також може бути здійснено загалом, у загальному методі перетворення.