Swift で 数値データなどを Data に固める

# DataStream

頻度はそんなにないものの、時々データをファイルに保存したい時などに、JSON や PropertyListSerialization などでなく バイナリー形式で保存したい時があります。とは言っても、struct の内容をそのまま、ファイルに書き込むのは、エンディアンの違いや、アライメントが異なる場合などにデータ交換に失敗する事が考えられます。

そこで、今回は DataStream というクラスを書いてみたので紹介したいと思います。DataStream には DataReadStream と DataWriteStream がありそれぞれ、読み込み用と書き込み用となっています。この Stream にデータを順時書き込み、または読み込みむ事で `Data` の形式と交換する事が出来るようになります。

## 数値データを Data に書き出す

`DataWriteStream` は書き込み専用です。これの `write()` で順時データが書き出されます。リトルエンディアンの環境では、ビッグエンディアンに変換されます。書き込み可能なデータの対象は、`UInt8`, `Int8`, `UInt16`, `Int16`, `UInt32`, `Int32`, `UInt64`, `Int64`, `Float`, `Double`, `Bool`, `Data` になります。アライメントは気にする必要はありません。

```.swift
let writeStream = DataWriteStream()
do {
    try writeStream.write(UInt8(0x01))
    try writeStream.write(UInt16(0x2345))
    try writeStream.write(UInt32(0x6789abcd))

    try writeStream.write(Int8(-120))
    try writeStream.write(Int16(-32000))
    try writeStream.write(Int32(-100_000))

    try writeStream.write(Float(0.5))
    try writeStream.write(Double.pi)

    try writeStream.write(true)
    try writeStream.write(false)
}
catch { ... }

if let data = writeStream.data {
    ...
}
```

こうして、`Data`に固めてから、ファイルに書き出せば、JSON などより効率的に、まとまった数値データなどを保存する事が可能になります。

## Data から数値データを取り出す

データの読み込みは、`DataReadStream` の `read()` メソッドを呼び出します。データの読み出しに失敗すると、エラーが投げられます。読み込む順番とデータの型は書き込んだ時と一致している必要があります。

```.swift
let readStream = DataReadStream(data: data)
do {
    let a = try readStream.read() as UInt8 // 0x01
    let b = try readStream.read() as UInt16 // 0x2345
    let c = try readStream.read() as UInt32 // 0x6789abcd

    let d = try readStream.read() as Int8 // -120
    let e = try readStream.read() as Int16 //  -32000
    let f = try readStream.read() as Int32 // -100_000

    let g = try readStream.read() as Float // 0.5
    let h = try readStream.read() as Double // Double.pi

    let h = try readStream.read() as Bool // true
    let i = try readStream.read() as Bool // false
}
catch { ... }
```

## 部分的にバイナリデータを扱う場合

文字列を保存する場合などは、文字列をバイナリに変換して保存・読み込みをする必要があります。そのまま保存すると、読み出しの時に何バイト読み込めばいいのか分からないので、そんな場合には読み込むバイト数をヘッダとして書き出した後、実際にバイナリのデータを書き込むといいでしょう。

```.swift
    let subdata = ...
    try writeStream.write(data.count)
    try writeStream.write(data)
```

そうすれば、読み込む時に文字列などが含まれるバイナリの部分が何バイトなのかがはっきりします。

```.swift
    let length = try readStream.read() as Int
    let subdata = try readStream.read(count: length) as Data
```

## 任意のデータを書き出す場合・読み込む場合

例えば、特定の構造体のデータを生のまま、書き出したい場合があったと仮定します。

```.swift
struct MyStruct {
    // ....
}
```

`DataWriteStream` のジェネリックの `writeBytes()` メソッドを呼び出せば、まとめて書き出せます。ただし、エンディアンやアライメントの問題があるので、デバイスやOSを超えてデータ交換する場合には適当ではない可能性があります。

また、`String` や `Data` をメンバーに持つ場合は、さらに `class` のインスタンスが含まれる場合も、単純に書き出せるわけではないので、十分気をつけましょう。

```.swift
    let myStruct: MyStruct ...
    try writeStream.writeBytes(myStruct)
```

そして、読み込む時は、`DataReadStream` の ジェネリックなメソッド `readBytes()` を呼び出します。

```.swift
    var myStruct = readStream.readBytes() as MyStruct
```

場合によっては、extension を用意して、利用したい型の読み込み、書き込みをが便利になる場合もあるかもしれません。

```.swift
extension DataWriteStream {
    func write(_ value: CGPoint) throws {
        let x = Double(value.x)
        let y = Double(value.y)
        try self.write(x)
        try self.write(y)
    }
}

extension DataReadStream {
    func read() throws -> CGPoint {
        let x = try self.read() as Double
        let y = try self.read() as Double
        return CGPoint(x: x, y: y)
    }
}
```

これで、頻繁に使う型はもう少しばかり便利になります。

```.swift
    let point: CGPoint = ...
    try writeStream.write(point)
```

```.swift
    let point = try readStream.read() as CGPoint
```

## 読み込み時に Stream の最後まで来たかどうかの判定

読み込み時に、Stream の最後に来たかどうかの判定は以下のようにします。

```.swift
    while readStream.hasBytesAvailable {
        // read more
    }
```

## コードは Git Hub から入手可能です。

https://github.com/codelynx/DataStream

## License

MIT License