các loại số Swift khứ hồi đến / từ Dữ liệu


95

Với Swift 3 nghiêng về phía Datathay vì [UInt8], tôi đang cố gắng tìm ra cách hiệu quả nhất / thành ngữ nhất để mã hóa / giải mã hoán đổi các loại số khác nhau (UInt8, Double, Float, Int64, v.v.) dưới dạng các đối tượng Dữ liệu.

câu trả lời này cho việc sử dụng [UInt8] , nhưng có vẻ như nó đang sử dụng các API con trỏ khác nhau mà tôi không thể tìm thấy trên Dữ liệu.

Về cơ bản tôi muốn một số tiện ích mở rộng tùy chỉnh trông giống như sau:

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

Phần thực sự khiến tôi lẩn tránh, tôi đã xem qua một loạt các tài liệu, là làm thế nào tôi có thể lấy một số loại con trỏ (OpaquePointer hoặc BufferPointer hoặc UnsafePointer?) Từ bất kỳ cấu trúc cơ bản nào (tất cả đều là số). Trong C, tôi chỉ cần vỗ một dấu và ở phía trước nó, và bạn sẽ làm được.


Câu trả lời:


259

Lưu ý: Mã hiện đã được cập nhật cho Swift 5 (Xcode 10.2). (Bạn có thể tìm thấy các phiên bản Swift 3 và Swift 4.2 trong lịch sử chỉnh sửa.) Cũng có thể dữ liệu chưa được căn chỉnh hiện được xử lý chính xác.

Cách tạo Datatừ một giá trị

Kể từ Swift 4.2, dữ liệu có thể được tạo từ một giá trị chỉ với

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

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

Giải trình:

  • withUnsafeBytes(of: value) gọi bao đóng với một con trỏ đệm bao phủ các byte thô của giá trị.
  • Con trỏ đệm thô là một chuỗi các byte, do đó Data($0)có thể được sử dụng để tạo dữ liệu.

Cách lấy một giá trị từ Data

Tính đến Swift 5, withUnsafeBytes(_:)các Dataviện dẫn việc đóng cửa với một “untyped” UnsafeMutableRawBufferPointerđể các byte. Các load(fromByteOffset:as:)phương pháp đọc giá trị từ bộ nhớ:

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

Có một vấn đề với cách tiếp cận này: Nó yêu cầu bộ nhớ được căn chỉnh thuộc tính cho kiểu (ở đây: căn chỉnh thành địa chỉ 8 byte). Nhưng điều đó không được đảm bảo, ví dụ: nếu dữ liệu được lấy dưới dạng một phần của Datagiá trị khác .

Do đó, sẽ an toàn hơn khi sao chép các byte sang giá trị:

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

Giải trình:

  • withUnsafeMutableBytes(of:_:) gọi bao đóng với một con trỏ đệm có thể thay đổi bao gồm các byte thô của giá trị.
  • Các copyBytes(to:)phương pháp DataProtocol(mà Datachiếu theo) bản byte từ các dữ liệu vào bộ đệm đó.

Giá trị trả về copyBytes()là số byte được sao chép. Nó bằng với kích thước của bộ đệm đích hoặc nhỏ hơn nếu dữ liệu không chứa đủ byte.

Giải pháp chung # 1

Các chuyển đổi trên hiện có thể dễ dàng được thực hiện như các phương pháp chung của 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
    }
}

Ràng buộc T: ExpressibleByIntegerLiteralđược thêm vào đây để chúng ta có thể dễ dàng khởi tạo giá trị thành “không” - đó không thực sự là một hạn chế vì phương thức này có thể được sử dụng với các kiểu “trival” (số nguyên và dấu phẩy động), xem bên dưới.

Thí dụ:

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

Tương tự, bạn có thể chuyển đổi mảng sang Datavà lùi:

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

Thí dụ:

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]

Giải pháp chung # 2

Cách tiếp cận trên có một nhược điểm: Nó thực sự chỉ hoạt động với các kiểu "tầm thường" như số nguyên và kiểu dấu phẩy động. Các loại "phức tạp" nhưArrayStringcó (ẩn) con trỏ đến bộ nhớ bên dưới và không thể được chuyển đi xung quanh bằng cách chỉ sao chép chính cấu trúc. Nó cũng sẽ không hoạt động với các kiểu tham chiếu chỉ là con trỏ đến bộ lưu trữ đối tượng thực.

Vì vậy, giải quyết vấn đề đó, người ta có thể

  • Xác định một giao thức xác định các phương thức chuyển đổi tới Datavà lui:

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
  • Triển khai các chuyển đổi làm phương thức mặc định trong tiện ích mở rộng giao thức:

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

    Tôi đã chọn một cái có sẵn khởi tạo có sẵn ở đây để kiểm tra xem số byte được cung cấp có khớp với kích thước của loại không.

  • Và cuối cùng tuyên bố sự phù hợp với tất cả các loại có thể được chuyển đổi qua Datalại một cách an toàn :

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

Điều này làm cho việc chuyển đổi trở nên thanh lịch hơn:

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

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

Ưu điểm của cách tiếp cận thứ hai là bạn không thể vô tình thực hiện các chuyển đổi không an toàn. Điểm bất lợi là bạn phải liệt kê tất cả các loại "an toàn" một cách rõ ràng.

Bạn cũng có thể triển khai giao thức cho các loại khác yêu cầu chuyển đổi không nhỏ, chẳng hạn như:

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

hoặc triển khai các phương thức chuyển đổi trong các loại của riêng bạn để làm bất cứ điều gì cần thiết để tuần tự hóa và giải mã hóa một giá trị.

Byte đặt hàng

Không có chuyển đổi thứ tự byte nào được thực hiện trong các phương pháp trên, dữ liệu luôn theo thứ tự byte chủ. Đối với biểu diễn độc lập với nền tảng (ví dụ: thứ tự byte “big endian” hay còn gọi là “mạng”), hãy sử dụng các thuộc tính số nguyên tương ứng. khởi tạo. Ví dụ:

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
}

Tất nhiên việc chuyển đổi này cũng có thể được thực hiện nói chung, theo phương pháp chuyển đổi chung.


Thực tế là chúng ta phải tạo một varbản sao của giá trị ban đầu có nghĩa là chúng ta đang sao chép các byte hai lần? Trong trường hợp sử dụng hiện tại của tôi, tôi đang chuyển chúng thành Cấu trúc dữ liệu, vì vậy tôi có thể chuyển appendchúng thành một dòng byte ngày càng tăng. Trong C thẳng, điều này dễ dàng như *(cPointer + offset) = originalValue. Vì vậy, các byte chỉ được sao chép một lần.
Travis Griggs

1
@TravisGriggs: Việc sao chép một int hoặc float có thể sẽ không liên quan nhiều nhất, nhưng bạn có thể làm những điều tương tự trong Swift. Nếu bạn có ptr: UnsafeMutablePointer<UInt8>thì bạn có thể gán cho bộ nhớ được tham chiếu thông qua một cái gì đó tương tự như UnsafeMutablePointer<T>(ptr + offset).pointee = valuetương ứng với mã Swift của bạn. Có một vấn đề tiềm ẩn: Một số bộ xử lý chỉ cho phép truy cập bộ nhớ được căn chỉnh , ví dụ như bạn không thể lưu trữ Int tại một vị trí bộ nhớ lẻ. Tôi không biết liệu điều đó có áp dụng cho bộ vi xử lý Intel và ARM hiện đang sử dụng hay không.
Martin R

1
@TravisGriggs: (tiếp) ... Ngoài ra, điều này yêu cầu một đối tượng Dữ liệu đủ lớn đã được tạo và trong Swift, bạn chỉ có thể tạo và khởi tạo đối tượng Dữ liệu, vì vậy bạn có thể có một bản sao bổ sung của 0 byte trong quá trình khởi tạo. - Nếu bạn cần thêm thông tin chi tiết thì tôi khuyên bạn nên đăng một câu hỏi mới.
Martin R

2
@HansBrende: Tôi e rằng hiện tại là không thể. Nó sẽ yêu cầu một extension Array: DataConvertible where Element: DataConvertible. Điều đó không thể xảy ra trong Swift 3, nhưng được lên kế hoạch cho Swift 4 (theo như tôi biết). So sánh "Sự phù hợp có điều kiện" trong github.com/apple/swift/blob/master/docs/…
Martin R

1
@m_katsifarakis: Có thể do bạn gõ nhầm Int.selfthành Int.Type?
Martin R

3

Bạn có thể nhận được một con trỏ không an toàn đến các đối tượng có thể thay đổi bằng cách sử dụng withUnsafePointer:

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

Tôi không biết cách nào để lấy một cái cho các đối tượng không thể thay đổi, bởi vì toán tử inout chỉ hoạt động trên các đối tượng có thể thay đổi.

Điều này được chứng minh trong câu trả lời mà bạn đã liên kết.


2

Trong trường hợp của tôi, câu trả lời của Martin R có ích nhưng kết quả lại bị đảo ngược. Vì vậy, tôi đã thực hiện một thay đổi nhỏ trong mã của anh ấy:

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

Vấn đề liên quan đến LittleEndian và BigEndian.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.