round trip Jenis nomor Swift ke / dari Data

97

Dengan Swift 3 lebih condong ke arah Dataalih-alih [UInt8], saya mencoba mencari tahu cara paling efisien / idiomatik untuk menyandikan / mendekode dengan cepat berbagai jenis angka (UInt8, Double, Float, Int64, dll) sebagai objek Data.

Ada jawaban ini untuk menggunakan [UInt8] , tetapi tampaknya menggunakan berbagai API penunjuk yang tidak dapat saya temukan di Data.

Pada dasarnya saya ingin beberapa ekstensi khusus yang terlihat seperti:

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

Bagian yang benar-benar luput dari saya, saya telah melihat-lihat sekumpulan dokumen, adalah bagaimana saya bisa mendapatkan semacam pointer (OpaquePointer atau BufferPointer atau UnsafePointer?) Dari struct dasar apa pun (yang semua angkanya adalah). Di C, saya hanya akan menampar ampersand di depannya, dan begitulah.

Travis Griggs
sumber

Jawaban:

262

Catatan: Kode telah diperbarui untuk Swift 5 (Xcode 10.2) sekarang. (Versi Swift 3 dan Swift 4.2 dapat ditemukan di riwayat edit.) Juga data yang mungkin tidak selaras sekarang ditangani dengan benar.

Cara membuat Datadari suatu nilai

Mulai dari Swift 4.2, data dapat dibuat dari nilai hanya dengan

let value = 42.13
let data = withUnsafeBytes(of: value) { Data($0) }

print(data as NSData) // <713d0ad7 a3104540>

Penjelasan:

  • withUnsafeBytes(of: value) memanggil closure dengan pointer buffer yang menutupi byte mentah dari nilai.
  • Sebuah pointer buffer mentah adalah urutan byte, oleh karena itu Data($0)dapat digunakan untuk membuat data.

Cara mengambil nilai dari Data

Pada Swift 5, withUnsafeBytes(_:)dari Datamemanggil penutupan dengan "untyped" UnsafeMutableRawBufferPointerke byte. The load(fromByteOffset:as:)metode membaca nilai dari memori:

let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes {
    $0.load(as: Double.self)
}
print(value) // 42.13

Ada satu masalah dengan pendekatan ini: Ini mensyaratkan bahwa memori adalah properti selaras untuk jenis (di sini: selaras dengan alamat 8-byte). Tapi itu tidak dijamin, misalnya jika data diperoleh sebagai bagian dari yang lainData nilai .

Oleh karena itu, lebih aman untuk menyalin byte ke nilai:

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

Penjelasan:

  • withUnsafeMutableBytes(of:_:) memanggil penutupan dengan penunjuk buffer yang bisa berubah yang mencakup byte mentah dari nilai.
  • The copyBytes(to:)metode DataProtocol(yang Datasesuai) salinan byte dari data ke buffer yang.

Nilai yang dikembalikan copyBytes()adalah jumlah byte yang disalin. Ini sama dengan ukuran buffer tujuan, atau lebih kecil jika data tidak berisi cukup byte.

Solusi generik # 1

Konversi di atas sekarang dapat dengan mudah diimplementasikan sebagai metode umum dari 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
    }
}

Batasan T: ExpressibleByIntegerLiteralditambahkan di sini sehingga kita dapat dengan mudah menginisialisasi nilai ke "nol" - itu sebenarnya bukan batasan karena metode ini dapat digunakan dengan jenis "trival" (bilangan bulat dan titik mengambang), lihat di bawah.

Contoh:

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

Demikian pula, Anda dapat mengonversi array menjadi Datadan kembali:

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

Contoh:

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]

Solusi generik # 2

Pendekatan di atas memiliki satu kelemahan: Ini sebenarnya hanya bekerja dengan tipe "sepele" seperti bilangan bulat dan tipe floating point. Jenis "kompleks" seperti Array danString memiliki (tersembunyi) petunjuk ke penyimpanan yang mendasarinya dan tidak dapat disebarkan hanya dengan menyalin struct itu sendiri. Ini juga tidak akan bekerja dengan tipe referensi yang hanya menunjuk ke penyimpanan objek nyata.

Jadi selesaikan masalah itu, seseorang bisa

  • Tentukan protokol yang mendefinisikan metode untuk mengubah ke Datadan kembali:

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
    
  • Menerapkan konversi sebagai metode default dalam ekstensi protokol:

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

    Saya telah memilih penginisialisasi yang dapat gagal di sini yang memeriksa bahwa jumlah byte yang diberikan sesuai dengan ukuran jenisnya.

  • Dan akhirnya nyatakan kesesuaian dengan semua jenis yang dapat dengan aman diubah menjadi Datadan kembali:

    extension Int : DataConvertible { }
    extension Float : DataConvertible { }
    extension Double : DataConvertible { }
    // add more types here ...
    

Ini membuat konversi menjadi lebih elegan:

let value = 42.13
let data = value.data
print(data as NSData) // <713d0ad7 a3104540>

if let roundtrip = Double(data: data) {
    print(roundtrip) // 42.13
}

Keuntungan dari pendekatan kedua adalah Anda tidak dapat secara tidak sengaja melakukan konversi yang tidak aman. Kerugiannya adalah Anda harus membuat daftar semua jenis "aman" secara eksplisit.

Anda juga dapat mengimplementasikan protokol untuk jenis lain yang memerlukan konversi non-sepele, seperti:

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

atau terapkan metode konversi sesuai jenis Anda sendiri untuk melakukan apa pun yang diperlukan sehingga membuat serial dan deserialisasi nilai.

Urutan byte

Tidak ada konversi urutan byte yang dilakukan dalam metode di atas, data selalu dalam urutan byte host. Untuk representasi independen platform (misalnya urutan byte "big endian" alias "jaringan"), gunakan properti integer resp. penginisialisasi. Sebagai contoh:

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
}

Tentu saja konversi ini juga dapat dilakukan secara umum, dengan metode konversi generik.

Martin R
sumber
Apakah fakta bahwa kita harus membuat varsalinan dari nilai awal, berarti kita sedang menyalin byte dua kali? Dalam kasus penggunaan saya saat ini, saya mengubahnya menjadi struct Data, sehingga saya dapat mengubahnya menjadi appendaliran byte yang terus bertambah. Dalam C lurus, ini semudah *(cPointer + offset) = originalValue. Jadi byte hanya disalin sekali.
Travis Griggs
1
@TravisGriggs: Menyalin int atau float kemungkinan besar tidak akan relevan, tetapi Anda dapat melakukan hal serupa di Swift. Jika Anda memiliki ptr: UnsafeMutablePointer<UInt8>maka Anda dapat menetapkan ke memori yang direferensikan melalui sesuatu seperti UnsafeMutablePointer<T>(ptr + offset).pointee = valueyang sangat sesuai dengan kode Swift Anda. Ada satu masalah potensial: Beberapa prosesor hanya mengizinkan akses memori yang selaras , misalnya Anda tidak dapat menyimpan Int di lokasi memori yang aneh. Saya tidak tahu apakah itu berlaku untuk prosesor Intel dan ARM yang saat ini digunakan.
Martin R
1
@TravisGriggs: (lanjutan) ... Ini juga memerlukan objek Data yang cukup besar telah dibuat, dan di Swift Anda hanya dapat membuat dan menginisialisasi objek Data, jadi Anda mungkin memiliki salinan tambahan nol byte selama inisialisasi. - Jika Anda membutuhkan detail lebih lanjut, saya sarankan Anda mengirim pertanyaan baru.
Martin R
2
@ HansBrende: Saya khawatir hal itu saat ini tidak memungkinkan. Ini akan membutuhkan extension Array: DataConvertible where Element: DataConvertible. Itu tidak mungkin di Swift 3, tetapi direncanakan untuk Swift 4 (sejauh yang saya tahu). Bandingkan "Kesesuaian bersyarat" di github.com/apple/swift/blob/master/docs/…
Martin R
1
@m_katsifarakis: Mungkinkah Anda salah mengetik Int.selfsebagai Int.Type?
Martin R
3

Anda bisa mendapatkan penunjuk yang tidak aman ke objek yang bisa berubah dengan menggunakan withUnsafePointer:

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

Saya tidak tahu cara mendapatkannya untuk objek yang tidak bisa diubah, karena operator masuk hanya bekerja pada objek yang bisa berubah.

Ini ditunjukkan dalam jawaban yang Anda tautkan.

zneak
sumber
2

Dalam kasus saya, jawaban Martin R membantu tetapi hasilnya terbalik. Jadi saya melakukan sedikit perubahan pada kodenya:

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

Masalahnya terkait dengan LittleEndian dan BigEndian.

Beto Caldas
sumber