Когда Swift 3 склоняется к Data
вместо [UInt8]
, я пытаюсь выяснить, какой наиболее эффективный / идиоматический способ кодирования / декодирования Swift различных типов чисел (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?) Из любой базовой структуры (а это все числа). В C я бы просто поставил перед ним амперсанд, и все готово.
источник
Ответы:
Примечание . Код обновлен для 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, the
withUnsafeBytes(_:)
ofData
вызывает закрытие с «нетипизированными»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()
- количество скопированных байтов. Он равен размеру целевого буфера или меньше, если данные не содержат достаточно байтов.Общее решение # 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 // 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]
Общее решение # 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 { } // 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 }
Конечно, это преобразование также может быть выполнено в общем методе преобразования.
источник
var
копию начального значения, что мы копируем байты дважды? В моем текущем варианте использования я превращаю их в структуры данных, чтобы можно было преобразоватьappend
их в растущий поток байтов. В прямом C это так же просто, как*(cPointer + offset) = originalValue
. Таким образом, байты копируются только один раз.ptr: UnsafeMutablePointer<UInt8>
то вы можете назначить указанную память через что-то вроде того,UnsafeMutablePointer<T>(ptr + offset).pointee = value
что близко соответствует вашему коду Swift. Есть одна потенциальная проблема: некоторые процессоры допускают только выровненный доступ к памяти, например, вы не можете хранить Int в нечетной области памяти. Я не знаю, относится ли это к используемым в настоящее время процессорам Intel и ARM.extension Array: DataConvertible where Element: DataConvertible
. Это невозможно в Swift 3, но планируется в Swift 4 (насколько мне известно). Сравните «Условные соответствия» в github.com/apple/swift/blob/master/docs/…Int.self
какInt.Type
?Вы можете получить небезопасный указатель на изменяемый объекты, используя
withUnsafePointer
:withUnsafePointer(&input) { /* $0 is your pointer */ }
Я не знаю, как получить его для неизменяемых объектов, потому что оператор inout работает только с изменяемыми объектами.
Это показано в ответе, на который вы указали.
источник
В моем случае ответ Мартина Р. помог, но результат был инвертирован. Поэтому я немного изменил его код:
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.
источник