Travis Griggs Travis Griggs - 5 months ago 62
Swift Question

round trip Swift numeric types to/from Data

With Swift3 leaning towards

Data
instead of
[UInt8]
, I'm trying to ferret out what the most efficient/idiomatic way to encode/decode swifts various number types (UInt8, Double, Float, Int64, etc) as Data objects.

There's this answer for using [UInt8], but it seems to be using various pointer APIs that I can't find on Data.

I'd like to basically some custom extensions that look something like:

let input = 42.13 // implicit Double
let bytes = input.data
let roundtrip = bytes.to(Double) // --> 42.13


The part that really eludes me, I've looked through a bunch of the docs, is how I can get some sort of pointer thing (OpaquePointer or BufferPointer or UnsafePointer?) from any basic struct (which all of the numbers are). In C, I would just slap an ampersand in front of it, and there ya go.

Answer

A possible solution:

extension Data {

    init<T>(from value: T) {
        var value = value
        self.init(buffer: UnsafeBufferPointer<T>(start: &value, count: 1))
    }

    func to<T>(type: T.Type) -> T {
        return self.withUnsafeBytes { (bytes: UnsafePointer<T>) -> T in
           bytes.pointee
        }
    }
}

Example:

let input = 42.13 // implicit Double
let bytes = Data(from: input)
print(bytes) // <713d0ad7 a3104540>
let roundtrip = bytes.to(type: Double.self)
print(roundtrip) // 42.13
print(roundtrip == input) // true

Some notes:

  • As @zneak already said, you can take the address only of a variable, therefore a variable copy is made with var value = value. In earlier Swift versions you could achieve that by making the function parameter itself a variable, this is not supported anymore.
  • Instead of withUnsafePointer() as in the referenced solution, it is easier to create the Data object with the (new) initializer

    public init<SourceType>(buffer: UnsafeBufferPointer<SourceType>)
    
  • Data does not have a bytes property to access its contents (as NSData did), but a generic method withUnsafeBytes() instead.

  • let bytes = input.data would require to define a data property on arbitrary types. That is not possible (as far as I know), therefore I suggested an inititializer for Data instead.
  • Like the solution How to convert a double into a byte array in swift? that you linked to, this works only for "simple" types (such as integers, floating point types). Array and String for example contain hidden pointers to the underlying storage and cannot be passed around like this.

Similarly, you can convert arrays of number types to Data and back:

extension Data {

    init<T>(fromArray values: [T]) {
        var values = values
        self.init(buffer: UnsafeBufferPointer<T>(start: &values, count: values.count))
    }

    func toArray<T>(type: T.Type) -> [T] {
        return self.withUnsafeBytes { (bytes: UnsafePointer<T>) -> [T] in
            [T](UnsafeBufferPointer(start: bytes, count: self.count/sizeof(T)))
        }
    }
}

Example:

let input: [Int16] = [1, Int16.max, Int16.min]
let bytes = Data(fromArray: input)
print(bytes) // <0100ff7f 0080>
let roundtrip = bytes.toArray(type: Int16.self)
print(roundtrip) // [1, 32767, -32768]
print(roundtrip == input) // true