Туди-назад швидкі типи номерів до / з даних


95

Коли 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?) З будь-якої базової структури (яка є всі числа). У C я б просто ляснув амперсанд перед ним, і ось я.


Відповіді:


259

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

Загальне рішення No1

Наведені вище перетворення тепер легко реалізувати як загальні методи 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]

Загальне рішення No2

Вищезазначений підхід має один недолік: він насправді працює лише з "тривіальними" типами, такими як цілі числа та типи з плаваючою комою. "Складні" типи типу 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. Отже, байти копіюються лише один раз.
Тревіс Гріггс,

1
@TravisGriggs: Копіювання int або float, швидше за все, не буде актуальним, але ви можете робити подібні речі в Swift. Якщо у вас є ptr: UnsafeMutablePointer<UInt8>тоді, ви можете призначити для вказаної пам'яті щось подібне, UnsafeMutablePointer<T>(ptr + offset).pointee = valueщо відповідає вашому коду Swift. Є одна потенційна проблема: Деякі процесори дозволяють отримувати доступ лише до вирівняної пам'яті, наприклад, ви не можете зберігати Int у непарному місці пам'яті. Не знаю, чи стосується це процесорів Intel і ARM, що використовуються зараз.
Martin R

1
@TravisGriggs: (продовження) ... Також для цього потрібно, щоб уже був створений досить великий об'єкт Data, і в Swift ви можете лише створювати та ініціалізувати об'єкт Data, тому у вас може бути додаткова копія нульових байт під час ініціалізація. - Якщо вам потрібні додаткові відомості, я б запропонував вам опублікувати нове запитання.
Martin R

2
@HansBrende: Я боюся, що наразі це неможливо. Це вимагало б extension Array: DataConvertible where Element: DataConvertible. Це неможливо в Swift 3, але заплановано на Swift 4 (наскільки мені відомо). Порівняйте "Умовні відповідності" у github.com/apple/swift/blob/master/docs/…
Martin R

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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.