Примечание . Код обновлен для 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)
Пояснение:
withUnsafeBytes(of: value)
вызывает закрытие указателем буфера, покрывающим необработанные байты значения.
- Необработанный указатель буфера представляет собой последовательность байтов, поэтому
Data($0)
может использоваться для создания данных.
Как получить значение из Data
Начиная с Swift 5, the 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)
У этого подхода есть одна проблема: он требует, чтобы память была выровнена по свойствам для типа (здесь: выровнена по 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)
Пояснение:
withUnsafeMutableBytes(of:_:)
вызывает закрытие с изменяемым указателем буфера, покрывающим необработанные байты значения.
copyBytes(to:)
Метод DataProtocol
(к которому Data
соответствует) копирует байты из данных в этом буфере.
Возвращаемое значение copyBytes()
- количество скопированных байтов. Он равен размеру целевого буфера или меньше, если данные не содержат достаточно байтов.
Общее решение # 1
Вышеупомянутые преобразования теперь могут быть легко реализованы как общие методы 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
let data = Data(from: value)
print(data as NSData)
if let roundtrip = data.to(type: Double.self) {
print(roundtrip)
} 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)
let roundtrip = data.toArray(type: Int16.self)
print(roundtrip)
Общее решение # 2
Вышеупомянутый подход имеет один недостаток: на самом деле он работает только с «тривиальными» типами, такими как целые числа и типы с плавающей запятой. «Сложные» типы, такие как 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 { }
Это делает преобразование еще более элегантным:
let value = 42.13
let data = value.data
print(data as NSData)
if let roundtrip = Double(data: data) {
print(roundtrip)
}
Преимущество второго подхода заключается в том, что вы не можете случайно выполнить небезопасные преобразования. Недостатком является то, что вам нужно явно перечислять все «безопасные» типы.
Вы также можете реализовать протокол для других типов, которые требуют нетривиального преобразования, например:
extension String: DataConvertible {
init?(data: Data) {
self.init(data: data, encoding: .utf8)
}
var data: Data {
return Data(self.utf8)
}
}
или реализовать методы преобразования в ваших собственных типах, чтобы делать все необходимое, чтобы сериализовать и десериализовать значение.
Порядок байтов
В приведенных выше методах преобразование порядка байтов не выполняется, данные всегда находятся в порядке байтов хоста. Для независимого от платформы представления (например, «обратный порядок байтов» или «сетевой» порядок байтов) используйте соответствующие целочисленные свойства соотв. инициализаторы. Например:
let value = 1000
let data = value.bigEndian.data
print(data as NSData)
if let roundtrip = Int(data: data) {
print(Int(bigEndian: roundtrip))
}
Конечно, это преобразование также может быть выполнено в общем методе преобразования.