LoginSignup
11
6

More than 3 years have passed since last update.

[Swift] DataはSubSequenceもDataなので…

Last updated at Posted at 2020-10-20

はじめに

DataSubSequenceData」という事実1。知らないとバグを作る。知っていてもついうっかりバグを作る。なるべくそのバグを減らそうというための記事。

実例から学ぶ

最初のバイトをゲットだ

Dataの最初のバイト(UInt8)を表示する関数を考えてみましょう:

printFirstByte.swift
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:)はどう実装すればよいのでしょうか?
一つの例としては、インデックスの値に頼らない方法を用いて実装することでしょう:

correct-printFirstByte.swift
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])")みたいな実装ではダメというのは上で見た通りです。渡されるdatastartIndex0とは限らないからです。

正しい方法としてはいくつかの実装例が思いつきます。

enumeration-解1.swift
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]) // 期待通り
enumeration-解2.swift
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回目)

まとめ

大切なのは「startIndex0とは限らない!」ということです。

おまけ

こういった仕様はDataに限ったことではないので、汎用的に使えるようRandomAccessCollectionを拡張するのも手です。

に実装例があるので参考までに…。

11
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
6