ไปกลับประเภทหมายเลข Swift ไปยัง / จาก Data


97

ด้วย Swift 3 ที่เอนเอียงไปทางDataแทนที่จะเป็น[UInt8]ฉันพยายามค้นหาวิธีที่มีประสิทธิภาพ / สำนวนที่สุดในการเข้ารหัส / ถอดรหัสจะเปลี่ยนประเภทตัวเลขต่างๆ (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 ฉันจะตบเครื่องหมายแอมเพอร์แซนด์ข้างหน้าแล้วก็ไป


คำตอบ:


262

หมายเหตุ:ตอนนี้รหัสได้รับการอัปเดตสำหรับ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

ในฐานะของสวิฟท์ 5 withUnsafeBytes(_:)ของDataจะเรียกปิดด้วย“untyped” 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เพิ่มข้อ จำกัดที่นี่เพื่อให้เราสามารถเริ่มต้นค่าเป็น "ศูนย์" ได้อย่างง่ายดายนั่นไม่ใช่ข้อ จำกัด เพราะวิธีนี้สามารถใช้กับประเภท "trival" (จำนวนเต็มและทศนิยม) ได้ดูด้านล่าง

ตัวอย่าง:

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

หรือใช้วิธีการแปลงในประเภทของคุณเองเพื่อทำสิ่งที่จำเป็นเพื่อทำให้เป็นอนุกรมและกำหนดค่าเริ่มต้นใหม่

คำสั่งไบต์

ไม่มีการแปลงลำดับไบต์ในวิธีการข้างต้นข้อมูลจะอยู่ในลำดับไบต์ของโฮสต์เสมอ สำหรับการแสดงที่เป็นอิสระจากแพลตฟอร์ม (เช่น "big endian" หรือที่เรียกว่า "network" byte order) ให้ใช้คุณสมบัติจำนวนเต็มที่สอดคล้องกัน initializers. ตัวอย่างเช่น:

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ๆกับ ดังนั้นไบต์จะถูกคัดลอกเพียงครั้งเดียว
Travis Griggs

1
@TravisGriggs: การคัดลอก int หรือ float ส่วนใหญ่อาจไม่เกี่ยวข้อง แต่คุณสามารถทำสิ่งที่คล้ายกันใน Swift ได้ หากคุณมีptr: UnsafeMutablePointer<UInt8>แล้วคุณสามารถกำหนดให้กับหน่วยความจำที่อ้างอิงผ่านสิ่งUnsafeMutablePointer<T>(ptr + offset).pointee = valueที่ใกล้เคียงกับรหัส Swift ของคุณ มีปัญหาหนึ่งที่อาจเกิดขึ้น: โปรเซสเซอร์บางตัวอนุญาตให้เข้าถึงหน่วยความจำแบบชิดเท่านั้นเช่นคุณไม่สามารถจัดเก็บ Int ที่ตำแหน่งหน่วยความจำแปลก ฉันไม่ทราบว่าใช้กับโปรเซสเซอร์ Intel และ ARM ที่ใช้อยู่ในปัจจุบันหรือไม่
Martin R

1
@TravisGriggs: (ต่อ) ... นอกจากนี้ยังกำหนดให้มีการสร้างวัตถุข้อมูลที่มีขนาดใหญ่เพียงพอและใน Swift คุณสามารถสร้างและเริ่มต้นวัตถุข้อมูลได้เท่านั้นดังนั้นคุณอาจมีสำเนาศูนย์ไบต์เพิ่มเติมในช่วง การเริ่มต้น. - หากคุณต้องการรายละเอียดเพิ่มเติมฉันขอแนะนำให้คุณโพสต์คำถามใหม่
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

ในกรณีของฉันคำตอบของ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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.