はじめに
「Data
のSubSequence
もData
」という事実1。知らないとバグを作る。知っていてもついうっかりバグを作る。なるべくそのバグを減らそうというための記事。
実例から学ぶ
最初のバイトをゲットだ
Data
の最初のバイト(UInt8
)を表示する関数を考えてみましょう:
import Foundation
func printFirstByte(of data: Data) {
if data.isEmpty {
print("からっぽだよ。")
} else {
print("最初のバイトは \(data[0]) だよ。")
}
}
let data = Data([0, 1, 2, 3])
printFirstByte(of: data) // -> "最初のバイトは 0 だよ。"
一見すると良さそうですねぇ。
では、次の場合はどうでしょう?
printFirstByte(of: data.dropFirst()) // -> "最初のバイトは 1 だよ。"と表示される??
では、実際に実行してみましょう。
どうでしたか?
手元の環境では"🚫illegal hardware instruction"で落ちました。
…何故でしょうか?
理由を知るために、まず
// printFirstByte(of: data.dropFirst())
print(data.dropFirst().startIndex)
としてみてください。
表示された数字はなんでしょう?
"1" が表示されたはずです。
つまり、data.dropFirst()
のインデックスは1
から始まっているので、printFirstByte(of:)
内のdata[0]
の行で(インデックス0
はout of boundsなので)クラッシュしたということになります。
SubSequence
(別名Slice
)を返す実装の多くは、まるまる内容をコピーするのではなく、「元のデータのこの部分」ということを指し示すようなインスタンスを返します。Data
も実際にそうなっていて、dropFirst()
は元のデータのstartIndex
を1増やしただけのインスタンスを返してきます2。
元のデータかスライスデータのどちらかに変更が加えられるときになって初めてコピーが行われます。
正しい実装例
以上を踏まえprintFirstByte(of:)
はどう実装すればよいのでしょうか?
一つの例としては、インデックスの値に頼らない方法を用いて実装することでしょう:
import Foundation
func printFirstByte(of data: Data) {
if let firstByte = data.first {
print("最初のバイトは \(firstByte) だよ。")
} else {
print("からっぽだよ。")
}
}
let data = Data([0, 1, 2, 3])
printFirstByte(of: data) // -> "最初のバイトは 0 だよ。"
printFirstByte(of: data.dropFirst()) // -> "最初のバイトは 1 だよ。"
ね、簡単でしょ?
バイトを列挙
スライスだろうとなんだろうと最初を0番目として表示したい場合は?
たとえば、[4, 5, 6, 7]
というバイト列で
#0: 4
#1: 5
#2: 6
#3: 7
と表示させたいとき…。
print("#\(i): \(data[i])")
みたいな実装ではダメというのは上で見た通りです。渡されるdata
のstartIndex
が0
とは限らないからです。
正しい方法としてはいくつかの実装例が思いつきます。
import Foundation
func printAllBytes(in data: Data) {
for ii in 0..<data.count {
print("#\(ii): \(data[data.startIndex + ii])") // startIndexを足さないとダメ
}
}
let sourceData = Data([0,1,2,3,4,5,6,7,8,9])
printAllBytes(in: sourceData[4...7]) // 期待通り
import Foundation
func printAllBytes(in data: Data) {
for (ii, byte) in data.enumerated() {
print("#\(ii): \(byte)")
}
}
let sourceData = Data([0,1,2,3,4,5,6,7,8,9])
printAllBytes(in: sourceData[4...7]) // 期待通り
ね、簡単でしょ?(2回目)
まとめ
大切なのは「startIndex
は0
とは限らない!」ということです。
おまけ
こういった仕様はData
に限ったことではないので、汎用的に使えるようRandomAccessCollection
を拡張するのも手です。
に実装例があるので参考までに…。