Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
5
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

はじめに

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を拡張するのも手です。

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
5
Help us understand the problem. What are the problem?