今回も引き続きお気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」で紹介されている事例を見ていきますね。前回に Collection
のインデックスを取得する場面について話をしましたけれど、具体的なコードを見てはいなかったので、今回は次に進む前にそのあたりを実際に書いて体験してみようと思います。よろしくお願いしますね。
——————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #260
00:00 開始 00:32 今回の展望 01:22 インデックスを伴う繰返処理の計算速度 03:08 zip が提供するのはオフセット 07:34 オフセットはインデックスにして使う 13:38 zip でインデックスをペアとして受け取る 15:20 順番が欲しいだけなら enumerated が最適 16:40 indexed の良さそうなところ 18:36 IndexedCollection 20:46 実際の処理速度を計測してみる(最初は失敗) 25:23 今度こそ正く計測してみる 26:29 実測も加味しつつ、読みやすさも加味しつつ 27:27 クロージング ———————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #260
では始めていきますね。今日も引き続き、Swiftコードをどう書き換えるかという内容です。個人的にとても気に入っているブログに載っている内容を見ていくと、いろいろと良い発見があります。
次の項からやろうと前回お話ししましたが、特に「コレクションのインデックスに関する操作」を取り扱います。イテラティブでもzipでもなく、インデックスを使う場面です。ここを実際にコードを見ながら進めることで、より理解が深まるのではないかと思います。インデックスを扱うという場面は、少なからず出てくるので、この考え方としてとても有用です。
まず気になるのが計算速度です。このブログのテーマとしても触れられている点ですが、自分はこれまであまり計算速度を気にしたことはありませんでした。ただ、ここで述べられているパフォーマンス評価については興味深いです。インデックスの扱いの速度についても、どの程度の差が出るのか、実際に調べてみようと思います。
ドキュメントを見ると、イテレーティブが速いことが分かりますが、インデックスやzipがどれほどの差を持つのか気になります。特に、インデックスを扱うときのパフォーマンスがどの程度影響するのかも確認したいところです。
具体的に、なぜzipを使わないのかについては、前回の最後に触れました。インデックスとオフセットを取り違えることが問題の根本にあると思います。0からのインデックスを扱う際は、スライスには向いていないこともあります。
ここで、インデックスとオフセットの勘違いが起きてしまう理由についても触れます。インデックスだと思って扱っている部分が、実際にはオフセットであるために誤解が生じるのです。これを避けるために、より明確なラベル付けを行うのが重要だと思います。
具体的にコードを見てみると、Enumerated
を使った場合の例を示します。
for (offset, element) in array.enumerated() {
print("Offset: \\(offset), Element: \\(element)")
}
このように、オフセットとエレメントが取得できます。しかし、offset
をインデックスだと勘違いすることがあるため、注意が必要です。ダブルパターンを使ってインデックスとエレメントを明示的に扱う例も見ていきます。
for (index, element) in array.enumerated() {
print("Index: \\(index), Element: \\(element)")
}
このようにインデックスとエレメントを分けて扱うことで、誤解を避けることができます。
このようにインデックス操作について理解を深めるとともに、実際のパフォーマンス評価も行っていきましょう。 とりあえず、このようなコードの構造になっています。例えば、要素が 7
と等しい場合に(これはあまり良いコードではないですが) remove(at: index)
のような操作をしています。このコードはあまり推奨されません。例えば、要素が nil
だった場合や他の特定の値である場合、クラッシュする可能性があります。そのため、逆順にするなどの工夫が必要です。reverse
メソッドを使うことがありますね。
前回も説明しましたが、この場合、インデックスと要素を取得するわけです。ここで重要なのは、オフセットという概念です。オフセットは整数型(Int
)で、その一方でインデックスは本来インデックス型です。この違いを意識しないといけません。型が異なるということは非常に重要で、それぞれの型が持つ意味や振る舞いも異なるからです。
typealias
やアソシエイティブタイプの概念を利用することで、一見同じようなイント型に見えても、実際には異なる意味を持っている場合があります。これを理解しておくことが重要です。したがって、remove(at: offset)
というコードは明らかに間違っています。
では、どのように処理すればよいかというと、前回も説明したように、values.startIndex
からオフセット分だけ移動させる方法があります。コードが長くてわかりにくいかもしれませんが、それが一つの方法です。また、レースコンディションが発生しないように、値型を使ってローカルスコープ内で安全に処理することも重要です。
例えば、let values = ...
として一度コピーを取ってから操作するということです。また、filter
メソッドを使うと、よりクリーンなコードを書くことができます。enumerated()
と filter
を組み合わせるとよりよいでしょう。
さらに、後ろから要素を操作する場合には、以下のようにすることができます。
for (offset, element) in values.enumerated().reversed() {
if element == 7 {
values.remove(at: values.index(values.startIndex, offsetBy: offset))
}
}
これにより、構造的に間違いのないコードを書くことができます。重要なのは、オフセットとインデックスの違いをしっかりと把握して、どんなコレクションにも対応できる柔軟で効率的なコードを書くことです。こうした意識を持つことで、論理的に正しいコードを書けるようになり、ソフトウェアの品質向上につながると思います。 難しい部分ですが、この微妙な差について説明しますね。とりあえず、ここではfor
ループで各要素を取り出して出力しています。そして、オフセットをインデックスとして使っている理由についてもいろいろ書いてあります。
zip
は2つのシーケンスをペアにして1つのシーケンスにする、非常にシンプルな操作です。ここで重要になってくるのは、インデックスをペアとして取得するという点です。例えば、インデックスと値をペアにしたりします。
具体的には、次のようにzip
を使います:
let array = ["a", "b", "c"]
for (index, value) in zip(array.indices, array) {
print("Index: \\(index), Value: \\(value)")
}
ここでは、array.indices
とarray
の2つのシーケンスをzip
でペアにしています。array.indices
はレンジとして機能し、コレクションの要素を順番に追っていきます。コレクションはシーケンスの拡張版のようなもので、シーケンスの一部として考えることができます。
zip
は要素を順番にロッキングさせていき、いずれかのシーケンスの要素がなくなった時点で終了します。このようにシンプルなツールなので、多くの場合、パフォーマンスに問題はありません。
この方法でインデックスを使うことで、まどろっこしい操作を避け、スムーズにコードを書けます。特に問題なく動作するはずです。 ソースコードを見たらイメージが浮かぶかと思いますが、そのほうがすごくしっくりきます。オフセットを取ってからインデックスを使うより、インラインで使うほうが直感的に思えます。個人的には、オフセットを使わずに indices
を活用する場面が多いですね。ただ、先頭から番号順の値が必要な場面では、わざわざインデックスにこだわらず、0からの番号が簡単に手に入るイテラブルな構造を使うこともあります。偶数番目の要素に特定の処理をしたいときなども同様です。インデックスで操作すると、偶数番目を取りにくくなったりしますから、その辺りは状況に応じた適切な選択が必要です。
インデックスにはどのような良さがあるのかと言うと、コードをパッと見たときに理解しやすい点です。例えば、以下のようにインデックスを使うと、読みやすくなります。
if element == 7 {
array.remove(at: index)
}
このように、手続き的な雰囲気が出ますが、これはインデックスで操作していることが容易に理解できるからです。逆に、ジップを使ってインデックスと値を繋ぐ場合、少し考えないといけないかもしれません。とは言え、最適化がされている可能性も考えられます。例えば、インデックス付きコレクションベースの場合も、シーケンスを2つ持っているより効率的なことがあります。
次に、パフォーマンスが気になる場合です。例えば、インデックスを使った方法と、イテラブルな構造を使った方法との速度を比較してみたくなることもあるでしょう。これには、ループを回しながら、開始と終了時の時間を測ることが考えられます。以下はその一例です。
let start = Date()
// 実行するコード
let end = Date()
let duration = end.timeIntervalSince(start)
print("Duration: \\(duration) seconds")
このように時間を測定すれば、各手法の速度の違いが分かります。ループは適切な回数に設定しておく必要があります。また、場合によっては繰り返しの回数や待機時間を調整することも大切です。たとえば、以下のようにする場合です。
for _ in 0..<100 {
// 計測する処理
}
このようにして時間を稼げるか試します。同様に、インデックスを使った処理と、イテラブルな構造を使った処理でそれぞれ時間を測定し、比較することができます。これにより、どちらがパフォーマンス的に優れているかを確認することができます。
具体的な時間測定のコードをいくつか試してみると、Swiftの標準ライブラリやコレクションの特性をより深く理解でき、適切な方法を選択する助けになります。 Googleのライブラリで予備セットに取る関数があるんですね。あとで見てみようと思います。関係しておくといいかもしれませんね。そうですね、確かに配列を長くするといいかもしれません。スレッドスリープをやめて、スリープが少し重たいので、時間を思い切り短くして要素を増やしてみたいな、それだけやって様子を見ました。
これで試してみると、一番行けそうですね。行けました。とりあえずこんな感じにして、スリープだとまずいのかどうかですね。スリープを取り外してみますか。デバッグビルドなら大丈夫でしょう。確かにスリープを抜かすと結構出てきましたね。なるほど、スリープが問題だったんですね。全然違ってきました。
そうすると、enumerated
が圧倒的に早くて、index
が早く、zip
がまあまあこの中では遅いですね。これだけ回してるから、普段どんな影響があるのか気になりますね。この辺りは気にしてみます。このくらいだったら読みやすさを優先してindex
でも読みやすいですけど、そのためにライブラリを入れるかどうかも考えに入れると、enumerated
の方が良いかもしれません。この辺りで考えてみてもいいですね。
時間になったので、これくらいにしておきます。あとでこれ終わってからデモでリリースビルドで試してみます。それでまた面白い結果が出たら、次回お知らせしますね。ではこんな感じで、次回は引き続き先ほどのプログラムの続きから見ていこうと思います。お疲れ様でした。ありがとうございました。