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
になります。アライメントは気にする必要はありません。
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()
メソッドを呼び出します。データの読み出しに失敗すると、エラーが投げられます。読み込む順番とデータの型は書き込んだ時と一致している必要があります。
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 { ... }
部分的にバイナリデータを扱う場合
文字列を保存する場合などは、文字列をバイナリに変換して保存・読み込みをする必要があります。そのまま保存すると、読み出しの時に何バイト読み込めばいいのか分からないので、そんな場合には読み込むバイト数をヘッダとして書き出した後、実際にバイナリのデータを書き込むといいでしょう。
let subdata = ...
try writeStream.write(data.count)
try writeStream.write(data)
そうすれば、読み込む時に文字列などが含まれるバイナリの部分が何バイトなのかがはっきりします。
let length = try readStream.read() as Int
let subdata = try readStream.read(count: length) as Data
任意のデータを書き出す場合・読み込む場合
例えば、特定の構造体のデータを生のまま、書き出したい場合があったと仮定します。
struct MyStruct {
// ....
}
DataWriteStream
のジェネリックの writeBytes()
メソッドを呼び出せば、まとめて書き出せます。ただし、エンディアンやアライメントの問題があるので、デバイスやOSを超えてデータ交換する場合には適当ではない可能性があります。
また、String
や Data
をメンバーに持つ場合は、さらに class
のインスタンスが含まれる場合も、単純に書き出せるわけではないので、十分気をつけましょう。
let myStruct: MyStruct ...
try writeStream.writeBytes(myStruct)
そして、読み込む時は、DataReadStream
の ジェネリックなメソッド readBytes()
を呼び出します。
var myStruct = readStream.readBytes() as MyStruct
場合によっては、extension を用意して、利用したい型の読み込み、書き込みをが便利になる場合もあるかもしれません。
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)
}
}
これで、頻繁に使う型はもう少しばかり便利になります。
let point: CGPoint = ...
try writeStream.write(point)
let point = try readStream.read() as CGPoint
読み込み時に Stream の最後まで来たかどうかの判定
読み込み時に、Stream の最後に来たかどうかの判定は以下のようにします。
while readStream.hasBytesAvailable {
// read more
}
コードは Git Hub から入手可能です。
License
MIT License