データとの間の往復Swift数値タイプ


94

Swift 3 Data[UInt8]ではなくに傾いているので、さまざまな数値型(UInt8、Double、Float、Int64など)をデータオブジェクトとしてエンコード/デコードする最も効率的/慣用的な方法を探し出そうとしています。

[UInt8]を使用するためこの答えがありますが、それは私がデータで見つけることができないさまざまなポインターAPIを使用しているようです。

基本的に、次のようなカスタム拡張機能をいくつか使用したいと思います。

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

私を本当に見逃しているのは、たくさんのドキュメントを調べてきましたが、基本的な構造体(すべての数値)からポインタの種類(OpaquePointerまたはBufferPointerまたはUnsafePointer?)を取得する方法です。Cでは、私はアンパサンドをその前で平手打ちするだけで、そこに行きます。


回答:


255

注:コードは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

Swift 5以降、withUnsafeBytes(_:)of DataUnsafeMutableRawBufferPointer、バイトに対して「型なし」のクロージャーを呼び出します。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

このアプローチには1つの問題があります。それは、メモリがそのタイプに合わせてプロパティで揃えられている必要があります(ここでは、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:)メソッドは、データからそのバッファーにバイトをコピーします。DataProtocolData

の戻り値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

上記のアプローチには1つの欠点があります。実際には、整数や浮動小数点型などの「自明な」型でのみ機能します。以下のような「複合」タイプArrayStringなるストレージに(隠された)ポインタを持っているだけで、構造体自体をコピーすることによって周り渡すことはできません。また、実際のオブジェクトストレージへの単なるポインタである参照型では機能しません。

だからその問題を解決し、

  • 変換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
}

2番目のアプローチの利点は、不注意で安全でない変換を実行できないことです。欠点は、すべての「安全な」タイプを明示的にリストする必要があることです。

次のような重要な変換が必要な他のタイプのプロトコルを実装することもできます。

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

または、独自の型に変換メソッドを実装して必要なことをすべて行うので、値をシリアル化および逆シリアル化します。

バイトオーダー

上記の方法ではバイトオーダー変換は行われず、データは常にホストバイトオーダーになります。プラットフォームに依存しない表現(「ビッグエンディアン」または「ネットワーク」バイトオーダーなど)の場合は、対応する整数のプロパティまたはそれぞれを使用します。初期化子。例えば:

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初期値のコピーを作成する必要があるという事実は、バイトを2回コピーすることを意味しますか?私の現在の使用例では、それらをData構造体に変換appendしているので、増加するバイトストリームに変換できます。ストレートCでは、これはと同じくらい簡単*(cPointer + offset) = originalValueです。したがって、バイトは一度だけコピーされます。
Travis Griggs

1
@TravisGriggs:intまたはfloatのコピーはおそらく関係ありませんが、Swiftでも同様のことができます。あなたが持っているなら、あなたはあなたのSwiftコードに密接に対応するptr: UnsafeMutablePointer<UInt8>ようなものUnsafeMutablePointer<T>(ptr + offset).pointee = valueを経由して参照されたメモリに割り当てることができます。潜在的な問題が1つあります。一部のプロセッサは整列されたメモリアクセスのみを許可します。たとえば、奇数のメモリ位置にIntを格納できません。現在使用されているIntelおよびARMプロセッサに当てはまるかどうかはわかりません。
マーティンR

1
@TravisGriggs:(続き)...また、これには十分に大きなDataオブジェクトがすでに作成されている必要があり、Swift ではDataオブジェクトのみを作成および初期化できるため、初期化。–詳細が必要な場合は、新しい質問を投稿することをお勧めします。
マーティン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

私の場合、マーティン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.