Dengan Swift 3 lebih condong ke arah Data
alih-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.
sumber
Jawaban:
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
Data
dari suatu nilaiMulai 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.Data($0)
dapat digunakan untuk membuat data.Cara mengambil nilai dari
Data
Pada Swift 5,
withUnsafeBytes(_:)
dariData
memanggil penutupan dengan "untyped"UnsafeMutableRawBufferPointer
ke byte. Theload(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 lain
Data
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.copyBytes(to:)
metodeDataProtocol
(yangData
sesuai) 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: ExpressibleByIntegerLiteral
ditambahkan 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
Data
dan 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
Data
dan 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
Data
dan 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.
sumber
var
salinan 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 menjadiappend
aliran byte yang terus bertambah. Dalam C lurus, ini semudah*(cPointer + offset) = originalValue
. Jadi byte hanya disalin sekali.ptr: UnsafeMutablePointer<UInt8>
maka Anda dapat menetapkan ke memori yang direferensikan melalui sesuatu sepertiUnsafeMutablePointer<T>(ptr + offset).pointee = value
yang 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.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/…Int.self
sebagaiInt.Type
?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.
sumber
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.
sumber