今回は再び脱線に戻って、お気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」で紹介されている事例の続きを見ていきますね。今日に見ていくところは、このブログを見つけるきっかけにもなって以前に詳しく眺めた
Collectionの
countと
endIndex` の話から、続けて順に眺めていきますね。よろしくお願いします。
————————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #259
00:00 開始 00:10 今回の展望 01:04 count と endIndex は別物として扱う 03:17 インデックスは必ず Int 型 05:23 count の計算量 05:53 endIndex の計算量は⋯? 06:38 インデックスは必ずしも Int 型にはならない 07:06 インデックスは 0 から始まるとは限らない 07:41 最後の要素を取得したいとき 08:44 インデックスを取得するいくつかの方法 09:56 配列が空のときは、インデックスの範囲も空 11:46 配列が空ではないことを前提とする方法 13:04 幾つかのインデックスを取る方法を比較してみる 14:56 dropLast ではインデックスを取得できない 16:18 count を使った最終要素取得の妥当性 17:33 目的のインデックスを得る方法 19:33 count を使った正しい最終要素の取得方法 21:44 インデックスは扱いが面倒 22:48 インデックスが Int 型になっている型は、それを前提に扱っていくのも大切かも 23:47 文字列型に見るインデックスの扱い 25:13 文字列のインデックス自体を加減算できるようにはならない 27:06 インデックスについてのまとめ 27:58 コレクションが空かを判定する 28:45 要素が0個かではなく、空かを表現 32:21 count == 0 と isEmpty のどちらが速いか問題 33:48 isEmpty の実装コード 35:06 Collection の反復処理でインデックスを取得する方法 36:00 Swift Algorithms って、よく使う? 37:18 enumerated では offset が得られる 39:40 間違いを避けるために zip と indices を使う手もあり 40:37 繰返処理でインデックスを取得する方法についての所感 41:25 クロージングと次回の展望 —————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #259
では、始めていきますね。今日は引き続き、前回の話題から少し脱線して進めていきます。個人的にはとても気に入ったブログ「スマートウィフト」の記事に関連する内容を続けて見ていくという感じになります。
まず、以前見たコードについてです。コレクションの count
と endIndex
についてのお話です。この話題は3回か4回目の勉強会で取り上げたことがあったので、その記憶と照らし合わせながら進めます。日が経っているので、気になる点があれば改めて見ていくつもりですが、特に問題はなさそうです。
最初に、count
と endIndex
の違いを意識する重要性について話します。もともと、このブログは count
の代わりに endIndex
を使うと計算量が速くなり、コレクションの要素数を効率的に取得できるという紹介でした。ただし、これについて物議を醸したため、やや曖昧な表現になっている部分もあります。とはいえ、count
と endIndex
の違いをしっかり意識して使い分けることが大事です。
ブログのタイトル通り、count
と endIndex
という2つの似たインスタンスプロパティが存在します。この「似た」という表現が重要で、ブログではおおむね似た役割を持つと記述されています。しかし、count
は要素の数を取得するものであり、endIndex
は最後の要素の次のインデックスを指すため、これを混同せずに扱うことが重要です。Swiftでは、この違いを明確に区別して使うことが求められています。
また、大事なポイントとして、count
は必ず Int
型です。これはSwift 3か4の頃の話で、アソシエイティブタイプとして Distance
が割り当てられていた時代がありましたが、最適化が進んだ結果、Distance
は Int
型になりました。現在でも、count
は必ず Int
型となっています。
実際にコレクションの定義を確認してみると、Distance
も Int
型として扱われていることがわかりますね。ディプリケーション(非推奨)となった結果、すべての距離の型は Int
型になっています。よって、count
は常に Int
型として扱われます。
ブログの問題に関連して、ランダムアクセスコレクションに準拠したコレクションであれば、計算量がO(1)で取得できますが、そうでなければ通常はO(n)となります。ここは重要なポイントで、必ずしも endIndex
を使ったカウントがO(1)であるとは限りません。APIデザインガイドラインによれば、endIndex
がO(1)であることが多いですが、必ずしもそうとは限らない点を押さえておくべきです。
以上のように、今日の話題は count
と endIndex
の違いをしっかり理解し、適切に使い分けることの重要性についてです。どちらもコレクションに対する操作において非常に有用なプロパティですが、その違いを理解して正確に使うことが、Swiftプログラミングの品質向上に寄与します。 インデックスを先頭からしかたどれないということもあるので、フォワードインデックスという言葉が出てきます。ここまでで十分話しているので、この点についてはよしとして、先に進みます。カウントは整数型(Int
)ですが、コレクションのエンドインデックスはインデックス型です。たとえば、String
の場合には抽象化されたインデックス型のString.Index
型が使われます。必ずしも整数型(Int
)にはなりません。ここのポイントを補足しておきます。
サブアレイでdropFirst
を使用すると、インデックスが1から始まるアレイが取れます。しかし、ここで得られるのはアレイスライスです。したがって、配列のエンドインデックスは整数型ですが、0から始まるとは限りません。また、要素を操作するとこれが影響することもあります。
エンドインデックスの違いから、コレクションの最後の要素にアクセスしたいときには、カウントではなくエンドインデックスを使用します。厳密に言うと、カウントを使ってインデックスでアクセスすることは意味的に微妙です。最終要素の1つ前のインデックスを取得してアクセスするのが正しい方法です。
この他にいくつかのインデックスの取り方があります。仕事の中で慣れた方法でインデックスを取りがちですが、適切な方法をおさらいしておくと良いでしょう。たとえば、配列の最後の要素を取りたい場合、エンドインデックスから1を引く方法があります。
また、インデックスアクセスの話をすると、エンドインデックスプラスなどで最終要素を取得できます。ただし、オプショナルインデックスも考慮しないとランタイムエラーが発生する可能性があります。そのため、たとえばif let index = array.indices.last
のように条件を設定してからアクセスするのが安全です。
これが基本的な考え方です。安全性を重視するのであれば、if !array.isEmpty
やif let lastValue = array.last
のように、要素の存在を確認しながらアクセスする方法が推奨されます。
このように、状況によっては異なる方法が適用される場合がありますが、基本的な考え方と適切な方法の選択が重要です。 条件判定した後に状態が変わってしまうと問題が出てきます。今回のケースでは7行目も同様ですね。values
へのアクセスが2回に抑えられている程度で、最終インデックスを取ってからその後values
が変わっていたら対処できません。そのため、11行目のような書き方が理想的です。
もちろん、データベースの同時アクセスによるレースコンディションの問題で、必ずしも正しい値が取れないこともあります。この3つの例ではどれも同様ですが、values
から最後の値さえ取れれば、その後は安心してvalues
を扱えます。必要になったタイミングでvalues.last
を取得するとタイミングが遅くなり厄介です。こうした点から考えると、11行目の方法が理想的でしょう。もし他に良いやり方があれば教えてください。
dropLast
についても話がありましたね。dropLast
は後ろを削ったものを返すので、今回のケースには適しません。もしvalues
が変更されている場合には意味がありません。dropLast
を使った場合、最後の要素とそれより手前の要素を分離してしまうので、全く異なる結果になります。
次にインデックスアクセスについて話しましたが、やはりカウントを使うと違和感があります。たまたまうまく動いたとしても、言語仕様上保証されていなければ安心できません。たまたま取れたというだけでは、信頼性に欠けます。
配列の仕様上、このように書いて成立するのであれば問題ないかもしれませんが、異なるケースではランタイムエラーになることもあります。そのため、インデックス型とInt
型の兼ね合いも注意が必要です。エンドインデックスがInt
型で計算が成り立っていますが、これは本来避けるべきです。
正しい方法としては、if values.isEmpty
の判定を行った後、values.endIndex - 1
などのように明示的にインデックスを指定する方が良いです。
もう少し汎用的な方法として、values.index(before: values.endIndex)
という書き方もあります。これで最後の要素のインデックスを安全に取得できます。このように書くことで、他の人にもわかりやすく、コードの保守性も高まります。
最後に、本来values.count
を使って最後のインデックスを取りたい場合には、この書き方が正しいです。また、読みにくくなってしまう場合は、必要に応じて分かりやすいコメントを追加するのも一つの方法です。 インデックスのオフセットバイの書き方ですが、これをよく使う人はそれほど多くないかもしれません。ただ、状況によっては便利ですので、使われることはあります。
次に、配列のインデックスが Int
型である場合についてです。例えば、配列の最後の要素にアクセスする際、index(before: endIndex)
や index(after: startIndex)
などを使うことになります。これは一貫性のある方法で、特にエラーハンドリングや万全を期すためによく使われます。それに対して、count - 1
を直接使うのはあまり好まれない方法ですね。ただし、どちらもコード自体は読みやすく、状況に応じて使い分けることが大事です。
Swiftのインデックスアクセスは、Swift 3までは簡単でしたが、インデックスを取り違えたりすると理屈が合わなくなるという問題がありました。そのため、現在の仕様ではインデックスが親のコレクションに依存するようになっています。これによって、インデックス操作をするたびに親コレクションの影響を受けることになり、少々面倒になっています。
文字列の場合、特にインデックス操作が煩雑ですね。文字列は抽象化されているため、インデックス操作が少々複雑になります。ただし、配列については Int
型のインデックスアクセスができるので、コードを読みやすく保つために、これを活用することが大切です。インデックス型がリテラルを受け取り、演算ができるようになるとより便利だと考えられます。
具体的に、テキストの最後の文字を取りたい場合、例えば以下のようなコードになります。
let text = "Hello, World!"
let lastIndex = text.index(before: text.endIndex)
let lastCharacter = text[lastIndex]
このように index(before:)
を使って手前のインデックスを取得するのが正しい方法です。しかし、配列とは異なり、文字列のインデックスは異なる型(String.Index
)を使います。文字列のインデックス処理が面倒な一因です。
UTF-8などのエンコーディングの違いによって、文字列内の各文字が異なるバイト数を持つ場合、インデックス計算がさらに複雑になります。このため、文字列操作では親のコレクション(文字列全体)を起点としてインデックスを操作する必要があります。
以上、インデックス操作についての話でした。ここで、コレクションのサイズを意識したインデックスの操作など、使い方の正確さを押さえておくと混乱せずに進められるでしょう。それでは、次の新しい項目に進みます。 コレクションが空かどうかを確認するときに、count == 0
ではなくisEmpty
を使用するのはよくある話です。これは、どこに着目するかによって変わってきます。例えば、配列や辞書(Dictionary)などで要素があるかどうかを確認する際に、count == 0
と書くことがありますが、isEmpty
に置き換えることができます。
isEmpty
というプロパティを知らないことも往々にしてあります。要素があるかないかを確認する際に、過去のプログラミング習慣ではcount
を取って比較する方法が主流でした。count == 0
と書くことは、つまり空かどうかを見ているわけですが、この表現は理解するために何ステップか踏む必要があります。
count == 0
という表現は手続き的なコードですが、リストにはisEmpty
というプロパティが用意されているため、これを使えば、例えば「リストが空か」というより英語っぽい表現に変えることができます。Swiftはこの表現を推奨しており、表現としても適切です。
計算量の観点からも、count
はO(n)ですが、isEmpty
はO(1)です。例えば、ランダムアクセスコレクションでなければ、count
はO(n)となります。isEmpty
の計算量はO(1)であると明記されていますので、空かどうかの判定がO(1)でできるのは確かに素晴らしいことです。
時折、isEmpty
とcount
のどちらが早いかという議論が発生しますが、これはランダムアクセスコレクションの場合、count
がO(1)であることが前提となっているためです。実際にデータを取ってnext
を呼び出し、それがnil
でないか判定するような場合もあります。この場合、インライン演算の方がCPUが得意なのでcount
の方が早くなる可能性がありますが、それはランダムアクセスコレクションだからです。
ここでの結論として、isEmpty
を使う方が適切でしょう。実際にisEmpty
の定義を見てみましたが、スタートインデックスとエンドインデックスが一致するかどうかで判定しており、この実装も上手ですね。 isEmpty
のインデックスのスタートとエンドは、コレクションの場合、オーダー1で取れるランダムアクセスコレクションですから、スタートとエンドは普通に取れますね。それでも、けっこう早いです。便利ですから、isEmpty
を使っていきましょう。使っているけど、注意しないと== 0
を事前に使ってしまうことがあります。それでも便利ですね。
次に進みますか。コレクションのインデックスを選ぶには、エナミュレーションでもチップでもなくインデックスを使う方法があります。これについては、ひと通り予習しておきましたが、どうなるかな。話が混乱しそうですが、インデックスというメソッドがあり、それがSwift AlgorithmというAppleがパッケージマネージャーで提供しているライブラリのメソッドを使うという話です。この方法のほうが早いからこだわりが強いということですが、Swift Algorithmって一般的に使われていますか?これをナチュラルに入れるものなのかなと疑問に思ったところです。
少なくともこれだけのために入れるというのは、ちょっと大げさかもしれません。パッケージマネージャーで追加するのは手間がかかりますから、あまり使わないかもしれません。特殊なセットを使うときは別ですが、このインデックスという方法がまとめられているけど、まとめて使うのはどうなんでしょうか。この記事を見たときに、自分はSwift Algorithmをナチュラルに入れるものか疑問に思ったんです。それよりは別の方法があるのではないかと思います。
また、イナミレーティブについても今日の話で応用が効くので、それだけ少し説明します。Swift Algorithmの普及については別として、ブログ的に言われていることは面白いです。エンドインデックスとカウントの話では、あまりスライスにこだわっていなかったようですが、ここには妙にスライスを気にしているようです。イナミレーティブを使うと、ゼロから始まる番号が入ります。なのでインデックスがずれてしまうという話になります。オフセットの方がおそらく正しいので、スタートインデックスからのオフセットでインデックスを取ってアクセスすれば問題ないでしょう。
結局、使い方さえ間違えなければ、エンドインデックスとカウントのインデックス、ディスタンスの意味を理解することで安全にアクセスできます。確かにCでの不確実性が怖いところはありますが、それでも判断次第ですね。速度を犠牲にしてでも、順番にアクセスしていく方法もあります。ジップを使って要素とインデックスを一緒に安全に処理する方法も考えられます。
最後に、時間になったので今回はここまでにして、次回ここから見ていきましょう。では、これで終わります。お疲れ様でした。ありがとうございました。