本日は再び寄り道をしまして、自分の中でとても興味の湧いた技術ブログ「その Swift コード、こう書き換えてみないか」を眺めていこうと思います。
その中でも今回は、特に関心を引く機会になった Collection
における count
と endIndex
の扱いについてのところに注目して、Swift のいろんな特色を窺ってみようと思います。こちらのブログには元々、紹介されていた内容に覚えた違和感から興味を持ったのですけれど、全体的な印象からその著者がとても丁寧に深く思考を巡らせているのが印象的で信頼感を覚えますので、今日だけに限らずしばらく日数をかけてブログ全体を見ていこうと思ってます。よろしくお願いしますね。
——————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #254
00:00 開始 00:10 とても興味の湧いた count と endIndex の話 01:49 丁寧で考え方も好印象なブログをたよりに、周辺視野を広げていきたい 03:27 要素数をどうやって得るかという話 05:50 要素数を取得するときの計算量 06:50 部分配列では endIndex が要素数を示すとは限らない 07:52 かつては endIndex で数を取るのは常識だった 08:51 count と endIndex とは型が異なる 10:00 部分配列は効率化される? 13:03 元の配列が先に解放されてもバッファーは延命 13:48 どの段階で複製したかの検証は失敗 19:02 Swift では endIndex で個数を得る場面自体がまずなさそう 21:08 個数を endIndex で得ようとしない気持ちも大切 22:21 RandomAccessCollection の count は O(1) 23:14 逆に count を endIndex の代わりに使う必要もないはず 24:10 結果より、どういう経緯でその発想に至ったかが大切 24:58 endIndex が O(1) とは限らない? 27:36 実体験に基づいた最適解として得られる場合も 30:21 count は数で、endIndex はインデックス、という判断材料 33:24 インデックスの数を取る方法もあるけれど⋯ 34:53 周囲の意見を取り入れ過ぎている感がもったいない気もする 39:28 クロージング ———————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #254
では、始めていきますね。今日は、個人的にお気に入りの話題についてお話しします。このブログ記事「Swift Codeを書き換えよう」という内容についてです。この勉強会自体は5月22日に開催されたもので、私はTwitterで知ったのですが、チェックしていなかったんです。それで、参加された方はいらっしゃいますか?どうやらいらっしゃるようですね。参加された方がいるということで、そのブログについて話していきますね。
このブログ記事には非常に多くの反響がありました。Twitterでもたくさんの反応が見られました。ただ、そのブログはすでに更新されており、元の原稿も残されているので、そちらを見ていきます。Swiftのコードの書き方について、計算量に着目してカウントではなくエンドインデックスを使ったほうが良いという提案が主な内容となっています。この話題は、特にソフトウェアのパフォーマンスに関するもので、とても興味深いです。
元のブログ記事は丁寧に書かれており、その考え方も非常に良いものでした。今回、外部からの反響を受けて更新されたことで、意図がより明確になりました。周りの意見が多少影響を及ぼした部分もあるかもしれませんが、全体としては非常に興味深い内容です。今回は、そのカウントとエンドインデックスについて焦点を当てて、そのブログ記事を通じて様々な関連事項についても学んでいきたいと思います。
まず、この議論が注目を浴びた内容について見ていきます。Swiftの配列(Array)や辞書(Dictionary)の要素数を取得する際に、通常はcount
を使いますが、エンドインデックスを使う方法も提案されています。この二つの方法について、以下のように説明されています。
たとえば、通常の配列で要素を取得したい場合、
let values = [1, 2, 3, 4]
この配列の要素数を取得したい場合、普通は values.count
を使います。しかし、このブログではエンドインデックスも同じ値を取得できることを示しています。つまり、
let count = values.endIndex
とすれば、同じ結果が得られます。しかし、配列の場合count
の計算量はO(1)で、エンドインデックスもO(1)なので、この場合どちらを使ってもパフォーマンス的には問題ありません。
次に、サブアレイ(SubArray)の場合について話を移します。サブアレイを取り扱う場合、
let subArray = values[2..<4]
ここで、要素数を取得する際にエンドインデックスを使うと、期待した数ではなく間違った数値が返ってくる可能性があります。具体的には、2から4までの要素を取得する場合、カウントで取得すると正しい要素数が得られますが、エンドインデックスを使うと予想外の結果になることがあります。
このように、エンドインデックスの使用には注意が必要です。そのため、確実に要素数を取得したい場面では、やはりcount
を使うべきです。人はケアレスミスを犯しやすいため、確実な方法を選ぶことが大切です。
今日はこのブログ記事を通じて、Swiftのコーディングにおける計算量やパフォーマンスについて理解を深めていきましょう。 確かに昔のやり方かもしれませんが、この方法もあります。ただ、自分の中での感覚としては、これは慣習や慣例として使うイメージがあります。しかし、今の時代は言葉の意味も大事にしていく時代なので、言葉の意味を尊重することが重要です。
あと、エンドインデックスについても重要なポイントがあります。エンドインデックスは中端のインデックス、つまり最後の次を指します。一方、カウントはバウンドのものですね。そのようにしてやっていこうと思います。
もう一つ重要なポイントは、テキストのカウントとテキストのエンドインデックスが全然違う結果をもたらすことです。例えば、最後のインデックスを取って、それで10文字未満かどうかを調べるというようなとき、エンドインデックスを使うと異なる結果が得られます。このようなことで一致比較ができないため、ここでの調べ方が重要です。
さらに、サブアレイについても少し触れておきます。サブアレイはコピーされず、参照を持っている状態です。この設計は非常に賢いと思います。例えば、内部的にはちゃんとコピーせずオリジナルの方を参照する感じです。ただし、サブアレイは書き込みができないのかどうか、これは実際に試してみる必要がありますね。
サブアレイに値を書き込んだ場合、その瞬間にコピーされるのか、どうなるのかを確かめたいところです。実際に試してみる方法として、例えば以下のようなコードを考えます。
let array = [Object]()
var subArray: [Object]? = array[0..<0]
ここでオブジェクトのインスタンスを作り、参照が保持されているかどうかを確認します。ただし、これだけではコピーされたかどうかは分かりませんので、このアプローチでは不十分でしょう。
一方で、オリジナルのアレイが解放されるのかを確認する方法も考えられます。これによって、サブアレイが内部で参照を保っているかどうかが分かります。ただ、メモリ管理の複雑な点が多いので慎重に確認していく必要があります。
このように、Swiftの言語仕様や内部の動作を理解するためには、実際に手を動かして検証することが非常に重要です。今後も実験や確認を通じて、より深い理解を目指しましょう。 ここはもう少し詳しく見ていかないといけない部分ですね。このインデックスの話に関してですが、コピーされるタイミングによってコピーされる場合とされない場合があります。この点については個人的には開放したかったのです。これもオプショナルにして、ここでビックリにして、ここでS
を使います。
まずここまでやってしまいます。この時に、ここですね。ここでT
に入れた後にT
をnil
にした時にどうなるか。ちょっと検証が間違っているかもしれませんね。あと、プレイグラウンドのところですが、ここも何かあるのかもしれません。プレイグラウンドでの検証は特に参照カウント周りではちょっと弱いです。それを踏まえた上で、後で調べてみますね。
今後の話題に戻りましょう。まず、endIndex
がcount
と一致することは限定的な条件です。通常の状況ではそのようなシチュエーションはあまりないです。Swiftでコレクションを使っているとき、count
がO(1)でない場合はほとんどありません。特にテキストではマルチバイト文字などに注意しなければならない場合があります。
さらに、endIndex
が機能していない場合があります。サブアレイを扱う場合でも、count
が通らないことがあります。また、レンジを変数で扱った場合にも同様の問題が発生します。レンジのcount
も微妙な表現です。レンジで数を取ったりするときもあります。結局、あまりcount
を使わないで、endIndex
を使わなければならない状況は少ない気がします。
そのため、個人的にはcount
を優先的に使うのが良さそうです。どのような場合でもendIndex
で数を取るのは避けるべきだと考えます。また、無理にcount
を使わないほうが良いというのもありますし、コード的にも誤解を招きやすいところがあります。
ブログには面白いことが書かれていて、なぜその結論に至ったのかという考え方が述べられています。まずおさらいとして、ランダムアクセスコレクションがO(1)かO(N)かの境目について補足がされています。元の原文では、アレイやディクショナリーがランダムアクセスコレクションであることを強調する仕方が少し間違っていたため、誤解を招いたようです。
アレイやディクショナリーはランダムアクセスコレクションなのでO(1)です。安心してcount
を使えます。この点について補足しておきます。要素数が必要な場合はcount
を使い、最後の要素のインデックスプラス1が必要な場合にはendIndex
を使用する可能性があります。しかし、わざわざendIndex
を持ち出すのは疑問が残ります。エンドインデックスはランダムアクセスコレクション以外ではO(N)になる可能性があります。
アップルのドキュメントには、エンドインデックスが必ずしもO(1)であるとは限らないと記載されています。この点を踏まえれば、必ずしもエンドインデックスを使う必要はないと言えるでしょう。 ただ、APIデザインガイドラインに完全にAppleのコードが載っているとしたらいいのですが、コメントに「プロパティはオーダー1を期待するから、オーダー1じゃないときにはオーダーをコメントに書いておくと良い」というガイドラインがあります。しかし、エンドインデックスについては書いていません。ドキュメントに書いていないということは、オーダー1で良いのではないかという発想がどこまで通じるかは不明です。明確に書いてない以上、最終的には分からないということに収めないといけません。
しかし、エンドから順番に取っていかないとダメなコレクションは普通に存在するはずです。そういうコレクションでは、エンドインデックスを取るためには最後まで行かないといけないので、オーダーnになります。その場合、カウントとエンドインデックスは基本的に対等です。全く一緒ではないですが、エンドインデックスは瞬時に取れるけど、エンドに到達するには時間がかかるというシーケンシャルアクセスの問題があります。
ランダムアクセスコレクションかどうかという点も考慮する必要があり、エンドインデックスのオーダーがどれくらいかという問題とも関係します。この計算量が低いと、混乱しやすくなるかもしれません。しかし、役割が異なるからといって、いつでも簡単に置き換えられるわけではありません。そんなわけで、Twiftではカウントとエンドインデックスの置き換えが多くの場面で可能だとされていますが、例外も存在します。
他の言語では、エンドインデックスとカウントが大差ない場合もあります。たとえば、特定の記号を使用するとリストのカウントが取れる言語もあり、プログラム内でどちらを使っても構わない場合もあります。しかし、Swiftのような環境では、配列が必ずゼロから始まるインデックスを持つため、このような置換は難しいです。
興味深いポイントとして、例えば「カウント」は必ずInt
ですが、エンドインデックスはコレクションの関連型インデックスになります。これも重要です。つまり、カウントは定数値として返されますし、エンドインデックスはコレクションの最大の要素に対するインデックスです。だから、ディクショナリーもたまたまInt
型でゼロから始まるインデックスが取られているに過ぎません。
こうして見ると、エンドインデックスで代用するのは難しいということが分かりますね。でも、実際に当てはめられる場面は多いということです。結局、コレクションの関連型に注目することが重要です。 そうやっていろいろと調べて、本気を掴んでいきながら、「だからどうだ」というふうに考えていく、その中で伺えるブログでとてもいいなと思ったことがあります。
例えば、ランダムアクセスコレクションを持っていて、ランダムアクセスコレクションがコレクションにつながります。コレクションにすると、この中にインデックスがあります。インデックスのアソシエイティブタイプがあって、これを取れないことがあるんですよね。
実際、インデックスを取りたい場合は indices
というプロパティがあります。これでカウントを取ることもできますが、やらないほうがいいかもしれません。同じようで違う可能性が微妙に増えそうですし、わざわざ5行目を取る必要もないでしょう。この indices
のカウントは文字列でもちゃんと取れます。文字列でもインデックスの数が分かりますが、断定できないことはやめたほうがいいですね。最適な解があるならそれを使いましょう。
とにかく、今言葉で言ったとおり、インデックスとカウントは違うということです。一つの話題からこういうことをいろいろと思い浮かべて、そこから知識が広がっていくのはとてもいいことだと思います。ただ、このブログの元々のタイトルは「カウントを endIndex
に書き換えてみないか」がテーマでした。それが周りの指摘を受けてタイトルが変更されたのが、もったいないと感じました。理論を持っていたのなら、それを踏まえた上で endIndex
に書き換える提案をしてほしかったと思います。最終的な意見が混ざると、理論が柔らかくなってしまいがちです。endIndex
を書くことを推奨することはしないとしても、考える材料としては面白いなと思いました。
このブログの記事も同様です。用途が違うことを意識することが大切です。count
を使う話の主旨が要素数を数える場面で endIndex
の方が早いという仮説があるなら、それを見せやすかったのではないかと思います。
ただ、endIndex
の方がこの場面で使えるという具体例がないと分かりにくいですね。カウントを使う代わりに全部使うという仮説を立てるのは面白いと思います。そのブログにはその内容が書かれていますよね。このブログの内容は次回の勉強会でもいい題材になると思いますし、順番に見ていく予定です。
また、次回までに修正の方向性を練ってみるのも面白いかもしれません。どちらにせよ、作者の考え方がわかるという点で非常に有益な文面でした。まとめると、全体的に丁寧な説明の良いブログでした。
時間になりましたので、今日はこれくらいにします。また次回も引き続き、脱線しながらいろいろな方法を見ていきたいと思います。
お疲れ様でした。ありがとうございました。