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

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


person Travis Griggs    schedule 25.06.2016    source источник
comment
stackoverflow.com/a/43244973/2303865   -  person Leo Dabus    schedule 04.10.2017


Ответы (3)


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

Пример:

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
}

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

person Martin R    schedule 25.06.2016
comment
Означает ли тот факт, что мы должны сделать var копию начального значения, что мы копируем байты дважды? В моем текущем варианте использования я превращаю их в структуры данных, поэтому я могу append их в растущий поток байтов. В прямом C это так же просто, как *(cPointer + offset) = originalValue. Таким образом, байты копируются только один раз. - person Travis Griggs; 26.06.2016
comment
@TravisGriggs: копирование int или float, скорее всего, не будет актуальным, но вы можете делать аналогичные вещи в Swift. Если у вас есть ptr: UnsafeMutablePointer<UInt8>, вы можете назначить указанную память через что-то вроде UnsafeMutablePointer<T>(ptr + offset).pointee = value, что близко соответствует вашему коду Swift. Есть одна потенциальная проблема: некоторые процессоры разрешают только выровненный доступ к памяти, например вы не можете хранить Int в нечетной ячейке памяти. Я не знаю, применимо ли это к используемым в настоящее время процессорам Intel и ARM. - person Martin R; 26.06.2016
comment
@TravisGriggs: (продолжение) ... Также для этого требуется, чтобы уже был создан достаточно большой объект Data, а в Swift вы можете только создать и инициализировать объект Data, так что у вас может быть дополнительная копия нулевых байтов во время инициализации. - Если вам нужна дополнительная информация, я предлагаю вам задать новый вопрос. - person Martin R; 26.06.2016
comment
Мне нравится универсальное решение №2, и я использую его. Но каждый раз, когда я добавляю индекс Data, это не работает. Есть ли элегантный способ заставить его работать с индексированными диапазонами данных? Например. Int32 (данные: вход [0 ... 3])? - person Travis Griggs; 28.06.2016
comment
@TravisGriggs: Int32(data: data.subdata(in: 0 ..< 3)) должно работать, но я не знаю, связана ли это с другой копией. Также должна быть возможность расширить вышеуказанные методы другим параметром, например Int32(data: data, atOffset: 4). - person Martin R; 28.06.2016
comment
@MartinR, как типы массивов будут работать с общим решением №2? - person Hans Brende; 11.11.2016
comment
@HansBrende: Боюсь, что в настоящее время это невозможно. Для этого потребуется extension Array: DataConvertible where Element: DataConvertible. Это невозможно в Swift 3, но планируется в Swift 4 (насколько мне известно). Сравните условные соответствия в github.com/apple/swift / blob / master / docs / - person Martin R; 11.11.2016
comment
Мне интересно, есть ли здесь какие-либо потенциальные проблемы с выравниванием байтов. Может случиться так, что приведение указателя приводит к возврату невыровненного указателя, который затем разыменовывается, например, как двойной, что вызывает ошибку памяти. Если это так, может быть, лучше предпочесть решение, которое байтом копирует исходные байты в цель? - person rghome; 27.01.2017
comment
@rghome: Это хороший момент. Я твердо предполагаю, что буфер вновь созданного объекта данных выровнен для всех скалярных типов, подобно тому, что возвращает malloc (), но я не знаю, гарантировано ли это. - person Martin R; 27.01.2017
comment
Я почти уверен, что недавно выделенный Data долго выровнен, и я вижу, что ваш код всегда получает / устанавливает данные только из всего объекта, так что все в порядке. Если у вас могут возникнуть проблемы, попробуйте повторно использовать части кода (как и я), чтобы получить / установить данные из смещений в Data. В этом случае вы не можете просто привести байтовый указатель к небайтовому указателю и разыменовать; вам нужно будет сделать байтовую копию в цель. Возможно, я отправлю ответ, если у меня что-то заработает. - person rghome; 27.01.2017
comment
Эти решения все еще актуальны для Swift 4? - person user965972; 26.06.2017
comment
@ user965972: Должны. Есть ли у вас с этим проблемы в Swift 4? - person Martin R; 26.06.2017
comment
@ user965972 Общее решение №2 отлично работает со Swift 4. - person Baran Emre; 11.03.2018
comment
Еще один приятный трюк, который вы хотите добавить к своему ответу. После настройки расширения вы можете создать изменяющуюся функцию добавления, которая принимает любой тип. - person Chris Garrett; 12.04.2018
comment
Я искал немного большей безопасности с помощью Generic Solution 1, поэтому добавил некоторые ограничения типа и предварительные условия func to<T>(_ type: T.Type) -> T where T: FixedWidthInteger, а затем внутри функции precondition(self.count == T.bitWidth >> 2). Это предотвращает повреждение памяти чем-то вроде data[0..4].to(UInt32.self) - person nteissler; 26.07.2018
comment
ой, это должно быть >> 3 - person nteissler; 27.07.2018
comment
Общее решение №1 дает сбой на Swift 4.2. - person Moebius; 13.12.2018
comment
@Moebius: Не могли бы вы предоставить более подробную информацию? У меня это работает (просто дважды проверил в Xcode 10.1, Swift 4.2). - person Martin R; 13.12.2018
comment
@MartinR Просто предупреждаю (потому что я знаю, что у вас есть много ответов, которые используют этот API), метод withUnsafeBytes на Data, который возвращает вам типизированный указатель не рекомендуется в Swift 5. Теперь он back="nofrerol"> необработанный указатель буфера на лежащие в основе байты. - person Hamish; 29.12.2018
comment
@Hamish: Спасибо за информацию, кажется, мне нужно обновить их все :( - Вы можете сказать мне, в чем причина отказа от поддержки? - person Martin R; 29.12.2018
comment
@MartinR В старом API было слишком легко случайно написать код, небезопасный для памяти - он был реализован с использованием assumingMemoryBound(to:), что может привести к неопределенному поведению для withUnsafeBytes , если пользователь получает два указателя несвязанного типа на базовый буфер данных. Даже если бы он был переключен на использование bindMemory(to:), это вызвало бы проблемы со стабильностью памяти с инициализатором init(bytesNoCopy:) Data. - person Hamish; 29.12.2018
comment
Дальнейшее обсуждение этого вопроса можно найти по адресу: forum.swift.org/t/ - person Hamish; 29.12.2018
comment
Недостаток этого ответа заключается в том, что для UInts вы должны использовать один из CFSwap, чтобы убедиться, что вы получили правильный порядок байтов, потому что этот ответ неявно загружает данные в порядке хоста. - person Max; 13.05.2019
comment
@Max: Думаю, я рассмотрел проблему порядка байтов в самом конце этого ответа. Существуют «собственные» методы Swift, которые можно использовать вместо CFSwapXXX. Пожалуйста, дайте мне знать, если что-то отсутствует или непонятно. - person Martin R; 13.05.2019
comment
@MartinR да, ты прав. Я не понимал, что UInt16(littleEndian:) делает то же самое, что и CFSwapInt16HostToLittle. Документация написана так, что я считаю ее запутанной, но они эквивалентны. - person Max; 13.05.2019
comment
В Swift 5 решение №1 не работает для Int любого типа (Int8, Int16 и т. Д.), Поскольку они не соответствуют ExpressibleByIntegerLiteral. Я не знаю, было ли соответствие никогда, или оно было удалено в более поздних версиях Swift. - person m_katsifarakis; 19.09.2019
comment
@m_katsifarakis: я почти уверен, что он работает со всеми целочисленными типами. let data = Data([1, 2]); if let value = data.to(type: Int16.self) { print(value) } компилируется и запускается, как ожидалось, в Xcode 11 с Swift 5. - person Martin R; 19.09.2019
comment
@MartinR в Xcode 10.2.1 Я получаю Instance method 'to(type:)' requires that 'Int.Type' conform to 'ExpressibleByIntegerLiteral'. Возможно, мне стоит обновить Xcode и попробовать еще раз. - person m_katsifarakis; 19.09.2019
comment
@m_katsifarakis: у меня больше нет Xcode 10.2.1, но он работает с Xcode 10.3, текущей выпущенной версией. - person Martin R; 19.09.2019
comment
@m_katsifarakis: Может быть, вы неправильно набрали Int.self как Int.Type? - person Martin R; 19.09.2019
comment
@MartinR вот и все! Такая глупая ошибка. Большое спасибо за вашу помощь и блестящее решение! - person m_katsifarakis; 19.09.2019
comment
@MartinR Здравствуйте, спасибо за отличное решение. Как насчет использования решения №2 для типов, не соответствующих ExpressibleByIntegerLiteral? Я использовал ваше решение №2 Swift 4 с типом Bool (и другие, например Struct), и оно работало нормально, но теперь я не могу его использовать, потому что они не соответствуют ExpressibleByIntegerLiteral. - person ciclopez; 02.11.2020
comment
@ciclopez: Один из вариантов - написать extension Bool: DataConvertible. - Это требование ExpressibleByIntegerLiteral на самом деле является обходным путем для того факта, что не существует протокола, которому соответствуют все «простые» типы. В Swift 5.3 могут быть лучшие решения, я подумаю над этим когда-нибудь ... - person Martin R; 02.11.2020

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

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

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

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

person zneak    schedule 25.06.2016

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

person Beto Caldas    schedule 09.12.2016