туда и обратно Типы чисел Swift в / из данных

97

Когда 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 я бы просто поставил перед ним амперсанд, и все готово.

Трэвис Григгс
источник

Ответы:

262

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

Общее решение # 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. Таким образом, байты копируются только один раз.
Travis Griggs
1
@TravisGriggs: копирование int или float, скорее всего, не будет иметь значения, но вы можете делать аналогичные вещи в Swift. Если у вас есть, ptr: UnsafeMutablePointer<UInt8>то вы можете назначить указанную память через что-то вроде того, UnsafeMutablePointer<T>(ptr + offset).pointee = valueчто близко соответствует вашему коду Swift. Есть одна потенциальная проблема: некоторые процессоры допускают только выровненный доступ к памяти, например, вы не можете хранить Int в нечетной области памяти. Я не знаю, относится ли это к используемым в настоящее время процессорам Intel и ARM.
Martin R
1
@TravisGriggs: (продолжение) ... Также для этого требуется, чтобы уже был создан достаточно большой объект данных, а в Swift вы можете только создавать и инициализировать объект данных, поэтому у вас может быть дополнительная копия нулевых байтов во время инициализация. - Если вам нужна дополнительная информация, я предлагаю вам задать новый вопрос.
Martin R
2
@HansBrende: Боюсь, что в настоящее время это невозможно. Для этого потребуется extension Array: DataConvertible where Element: DataConvertible. Это невозможно в Swift 3, но планируется в Swift 4 (насколько мне известно). Сравните «Условные соответствия» в github.com/apple/swift/blob/master/docs/…
Мартин Р
1
@m_katsifarakis: Может быть, вы неправильно набранный , Int.selfкак Int.Type?
Martin R
3

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

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

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

Это показано в ответе, на который вы указали.

знеак
источник
2

В моем случае ответ Мартина Р. помог, но результат был инвертирован. Поэтому я немного изменил его код:

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.

Бето Калдас
источник