1. ホーム
  2. swift

[解決済み] ラウンドトリップスウィフト番号の種類は、データとの間で

2023-01-03 13:41:32

質問

Swift 3が傾きつつある中 Data の代わりに [UInt8] 私は、データオブジェクトとして様々な数値型(UInt8、Double、Float、Int64など)をエンコード/デコードする最も効率的で慣用的な方法を見つけ出そうとしています。

そこには UInt8]を使用するためのこの答えがあります。 がありますが、Dataで見つからない様々なポインタAPIを使用しているようです。

基本的には以下のようなカスタムエクステンションを使いたいと思います。

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

ドキュメントの束に目を通しましたが、本当に理解できない部分は、基本的な構造体(すべての数値がそうです)からある種のポインタ(OpaquePointer、BufferPointer、UnsafePointerなど)をどうやって取得できるかということです。C では、その前にアンパサンドを置くだけで、それで十分です。

どのように解決するのですか?

注意してください。 のコードが更新されました。 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(_:) Data は、クロージャを "型なし "で呼び出します。 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 は、値を簡単に「ゼロ」に初期化できるようにするためにここに追加されます。このメソッドはとにかく「3値」(整数および浮動小数点)型で使用できるため、実際には制限ではありません。

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つの欠点があります:それは実際に整数や浮動小数点型のような "trivial" のような複雑な型では動作しません。 ArrayString は、基礎となるストレージへの(隠れた)ポインタを持っているので 構造体そのものをコピーするだけでは渡せません。また また、実際のオブジェクト ストレージへの単なるポインターである参照型でも動作しません。

そこで、この問題を解決するために、1つの方法として

  • に変換するための方法を定義したプロトコルを定義します。 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番目のアプローチの利点は、安全でない変換を不用意に行うことができないということです。デメリットは、すべての "safe" 型を明示的にリストアップする必要があることです。

のような自明でない変換を必要とする他の型のためにプロトコルを実装することもできます。

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
}

もちろん,この変換は一般的な方法で行うこともできます。 変換メソッドで行うこともできます。