Туда и обратно типы номеров Swift в / из данных
С Swift 3 склоняется к Data
вместо [UInt8]
Я пытаюсь выяснить, какой самый эффективный / идиоматический способ кодирования / декодирования выполняет различные типы чисел (UInt8, Double, Float, Int64 и т. Д.) В качестве объектов данных.
Есть ответ на использование [UInt8], но, похоже, он использует различные API указателя, которые я не могу найти в Data.
Я хотел бы в основном некоторые пользовательские расширения, которые выглядят примерно так:
let input = 42.13 // implicit Double
let bytes = input.data
let roundtrip = bytes.to(Double) // --> 42.13
Часть, которая действительно ускользает от меня, я просмотрел кучу документов, это то, как я могу получить что-то вроде указателя (OpaquePointer или BufferPointer или UnsafePointer?) Из любой базовой структуры (которой являются все числа). В Си, я бы просто ударил амперсанд перед ним, и ты пойдешь.
3 ответа
Примечание: ответ (первоначально написанный для Swift 3) был обновлен для Swift 4.2/Xcode 10, что сделало возможными некоторые упрощения. Некоторые из моих предыдущих предложений вызывали неопределенное поведение, это было исправлено. Я также добавил замечания о порядке байтов и выравнивании.
Как создать 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)
может быть использован для создания данных.
До Swift 4.2 необходимо было сначала создать изменяемую копию:
let value = 42.13
var mutableValue = value
let data = withUnsafeBytes(of: &mutableValue) { Data.init($0) }
Как получить значение из Data
NSData
был bytes
свойство, чтобы получить доступ к базовому хранилищу. struct Data
имеет общий withUnsafeBytes(_:)
вместо этого метод, который можно использовать так:
let data = Data(bytes: [0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes { (ptr: UnsafePointer<Double>) -> Double in
return ptr.pointee
}
print(value) // 42.13
Если ContentType
может быть выведен из контекста, тогда его не нужно указывать в замыкании, поэтому это можно упростить до
let data = Data(bytes: [0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value: Double = data.withUnsafeBytes { $0.pointee }
print(value) // 42.13
Общее решение № 1
Вышеуказанные преобразования теперь могут быть легко реализованы как общие методы struct Data
:
extension Data {
init<T>(from value: T) {
self = Swift.withUnsafeBytes(of: value) { Data($0) }
}
func to<T>(type: T.Type) -> T {
return self.withUnsafeBytes { $0.pointee }
}
}
Пример:
let value = 42.13 // implicit Double
let data = Data(from: value)
print(data as NSData) // <713d0ad7 a3104540>
let roundtrip = data.to(type: Double.self)
print(roundtrip) // 42.13
Точно так же вы можете конвертировать массивы в Data
и назад:
extension Data {
init<T>(fromArray values: [T]) {
self = values.withUnsafeBytes { Data($0) }
}
func toArray<T>(type: T.Type) -> [T] {
return self.withUnsafeBytes {
[T](UnsafeBufferPointer(start: $0, count: self.count/MemoryLayout<T>.stride))
}
}
}
Пример:
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]
Общее решение № 2
Вышеупомянутый подход имеет один недостаток: как в Как преобразовать двойной массив в байтовый массив в Swift? фактически он работает только с "простыми" типами, такими как целые числа и типы с плавающей точкой. "Сложные" типы, такие как Array
а также String
имеют (скрытые) указатели на базовое хранилище и не могут быть переданы путем простого копирования самой структуры. Это также не будет работать со ссылочными типами, которые являются просто указателями на реальное хранилище объектов.
Так что решить эту проблему можно
Определите протокол, который определяет методы для преобразования в
Data
и назад:protocol DataConvertible { init?(data: Data) var data: Data { get } }
Реализуйте преобразования как методы по умолчанию в расширении протокола:
extension DataConvertible { init?(data: Data) { guard data.count == MemoryLayout<Self>.size else { return nil } self = data.withUnsafeBytes { $0.pointee } } 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 self.data(using: .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
}
Конечно, это преобразование может также быть сделано вообще, в общем методе преобразования.
центровка
Вышеуказанные методы для извлечения значения из Data
все предполагают, что данные правильно выровнены для типа значения. Если это не гарантировано, subdata(in:)
может использоваться для создания копии с выровненным базовым хранилищем.
Вы можете получить небезопасный указатель на изменяемые объекты, используя withUnsafePointer
:
withUnsafePointer(&input) { /* $0 is your pointer */ }
Я не знаю, как получить его для неизменяемых объектов, потому что оператор inout работает только с изменяемыми объектами.
Это продемонстрировано в ответе, с которым вы связались.
В моем случае ответ Martin R помог, но результат был инвертирован. Поэтому я сделал небольшое изменение в его коде:
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))
}
}
Проблема связана с LittleEndian и BigEndian.