Подтвердить что ты не робот

Круглые поездки Типы данных Swift в/из данных

С Swift 3, наклоняющимся к Data вместо [UInt8], я пытаюсь выяснить, какой самый эффективный/идиоматический способ кодирования/декодирования swifts различного рода типов (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?) из любой базовой структуры (в которой все числа). В С, я бы просто ударил амперсанда перед ним, и там я пойду.

4b9b3361

Ответ 1

Примечание. Код был обновлен для версии 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(_:) из 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() - это количество скопированных байтов. Он равен размеру целевого буфера или меньше, если данные не содержат достаточно байтов.

Общее решение № 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, чтобы мы могли легко инициализировать значение как "ноль" - это на самом деле не является ограничением, поскольку этот метод в любом случае можно использовать с типами "trival" (целочисленные и с плавающей запятой), см. ниже.

Пример:

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)
    }
}

или реализуйте методы преобразования в ваших собственных типах, чтобы делать что угодно необходимо сериализовать и десериализовать значение.

Порядок байтов

В приведенных выше методах преобразование порядка байтов не выполняется, данные всегда находятся в порядок байтов хоста. Для независимого от платформы представления (например, порядок байтов 'big endian' (он же 'сетевой'), используйте соответствующее целое число свойства соотв. Инициализаторы. Например:

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
}

Конечно, это преобразование также может быть сделано в общем, в общем метод конвертации.

Ответ 2

Вы можете получить небезопасный указатель на изменяемые объекты с помощью withUnsafePointer:

withUnsafePointer(&input) { /* $0 is your pointer */ }

Я не знаю способа получить один для неизменяемых объектов, потому что оператор inout работает только с изменяемыми объектами.

Это показано в ответе, с которым вы связались.

Ответ 3

В моем случае ответ 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.