https://www.youtube.com/watch?v=UYk2_pFZxVM
A Swift Tour
の「配列と辞書」について、前回はそれらから自分がイメージするところを詳細に見ていったので、今回は題材の The Swift Programming Language に沿ってもう少し基本的なところを眺めてみようと思います。それが終わったら、次のセクション「制御構文」の話に移っていこうと思ってます。よろしくお願いしますね。
———————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #31
00:00 開始 04:43 インデックスやキーを添えて使用する 05:21 添字構文 06:04 ゼロから始まるインデックス 06:48 Array.Index 07:13 文字のコレクション 07:56 String.Index 09:26 index(after:) 09:59 index(_:offsetBy:) 10:48 subscript 13:16 辞書リテラル 19:25 UTF16View 20:05 UTF8View 20:30 添字構文でラベルを使う 24:35 文字列のインデックスアクセス 30:25 配列リテラルで最終要素の後にカンマがあっても良い 31:40 順番の入れ替えが容易 36:42 配列の自動拡張 37:44 空のリテラル 38:58 配列や辞書の仕組み 39:38 Sequence 40:33 IteratorProtocol 41:20 Collection 42:55 MutableCollection 43:35 RangeReplaceableCollection 47:30 RandomAccessCollection 47:54 シーケンシャルアクセス 49:20 ランダムアクセス 53:32 クロージング ————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #31
【スライドについて】
ええと、今日は配列と辞書の話をしようか迷っていたんですが、やっぱりやりましょう。最近、自分がこの勉強会にだいぶ慣れすぎてしまって、いきなり話を突っ走りすぎたところがあるので、もう少しスライドに沿って話を進めたほうがいいんじゃないかと思いました。そんな感じで、基本的なところを話していきます。ただ、皆さんがイメージする基本とは少し違うかもしれませんが、せっかく内容がいろいろ書いてあるので、見ていきましょう。復習にもなります。
配列や辞書は角括弧を使って生成します。前回これについて徹底的に話しましたので、気になる人は前回の内容を見てください。アクセスする際には、角括弧にインデックスやキーを添えて使用します。これが現在表示されている一番下のコードですね。例えば、変数 occupation
に対してアクセスするときも、ブラケットを使います。occupation
の意味は確か「職業」でしたね。そんな感じの変数に対して、新しいキーに新しい値を設定するという例です。これは辞書の特徴の一つで、キーとバリューのセットで読み書きができるのが特徴です。
この例では、キーが文字列で、アクセスする際にはキーを使っています。これは辞書の特徴で、配列の場合はインデックスを使ってアクセスします。これが配列の仕様ですね。大事な点は、辞書と配列のどちらも角括弧を使ってアクセスできるという漠然とした情報がこのスライドにありますが、もう少し詳しく見ていくことで理解が深まると思います。
ここで余談ですが、administrator
という単語をタイプするのに最初苦労しました。特にWindowsユーザー、特にWindowsサーバーをいじっている人なら分かるかと思いますが、自分も一生懸命覚えた記憶があります。
さて、インデックスやキーを添えて使用する具体的な仕組みについておさらいしましょう。配列がインデックスを使い、辞書がキーを使うというのは、それぞれの型がアクセスする手段として採用しているだけです。この括弧でのアクセスは、サブスクリプト(添字構文)という構文を使っています。この話はまたいずれ詳しく出てくると思いますので、軽く紹介するだけにとどめます。
アレイ(配列)は要素が順番に並んでいるもので、インデックスアクセスが可能です。Twift言語では配列は0から始まるインデックスが約束ごとになっています。例えば、Fortranなどではインデックスの開始ポイントと終了ポイントを自由に設定できますが、Twiftでは0から始まるというのが重要なポイントです。最初の要素を取りたいときは0、2番目の要素を取りたいときは1というようにアクセスします。このインデックスには、アレイ型に IndexType
というのがあり、イント型が採用されています。 これがストリング型も面白い性格があって、Swiftではこの勉強会でも以前にお話ししたことがあるので、思い出せる人は思い出してほしいんですけど、文字列は文字のコレクションという扱いになっています。だからこれもコレクションになっているので、通し番号でアクセスができるという風になっています。
ただ、ストリングのインデックスは Int
型ではないというところが大事なポイントです。今は軽く知っておいてもらえれば十分ですけど、ストリングインデックス型というものになっているので、ストリングに対して Int
型で例えば「2番目の文字」みたいな風には参照できません。これをやりたい時にはSwiftのコレクションの仕組みは若干複雑なので書きにくいんですけど、例えば「2番目の値を取りたい」とすると、
let string = "example"
let index = string.index(string.startIndex, offsetBy: 2)
let character = string[index]
のように書くことができます。最初のインデックス(startIndex
)から2個進んだインデックスを取得して、そのインデックスを使ってアクセスします。これで2番目の文字が取得できます。まるで足し算をしているかのようですが、コレクションのインデックスは整数ではなく、コレクションに特有の型であるということです。
これは添え字構文とサブスクリプト機能を利用して実現されています。例えば、自分でバリュー型を作ってサブスクリプトを定義してあげると、添え字を使ったアクセスが可能になります。以下の例を見てください。
struct Value {
subscript(location: Bool) -> Int {
if location {
return 1
} else {
return -1
}
}
}
let value = Value()
print(value[true]) // 1
print(value[false]) // -1
このようにして、バリュー型のインスタンス化したインスタンスに対して添え字を用いてアクセスすることができます。このサブスクリプト機能がディクショナリー型でも使われており、キーを使って対応する値を取得することができます。
let dictionary = ["a": 1, "b": 2, "c": 3]
print(dictionary["a"]) // Optional(1)
このように、ディクショナリー型も添え字を使ってキーアクセスが可能で、それによって対応する値を取得できます。
ここで大事なのは、同じ括弧記号を使いますが、配列のインデックスアクセスなのか、ディクショナリーのキーアクセスなのかという点です。Swiftではこれらを区別しつつ、自然に使えるようにデザインされています。
以上、今回の話をおさらいすると、配列や辞書の値は括弧を使って生成できるのはリテラル構文のおかげです。配列リテラルと辞書リテラルですね。それぞれの特性を理解して使いこなせるようにしましょう。 そのおかげで、生成時にアクセスする際にも各カッコにインデックスを添えて使用できるのは、サブスクリプト添え時効分のおかげです。異なる仕組みによって同じような書き方が提供されているのは、個人的にはとても興味深いポイントです。
コメントが増えているような気がしますね。「ストリングインデックスアフター」というコメントを見かけました。文字列のインデックスを操作する際に、適切な方法を使わないとおかしくなるかもしれませんが、構文としては型が一致しているのでOKですね。ただ、結果として文字化けする可能性もあります。試してみると面白そうですが、今日取り扱う配列と辞書が落ち着いたらやってみてもいいかもしれませんね。
例えば、絵文字を使用してインデックス操作を試してみると、いろいろな結果が得られて面白いかもしれません。ちなみに、インデックス操作に失敗するとクラッシュしたり、異なるメモリアドレスにアクセスしてしまったりすることがあります。これは、ストリングインデックスが最適化されているためで、ヒープ領域内の文字列メモリーアドレスを正確に管理する必要があるからです。
また、インデックスを保持しておいて、文字列を変更した後でも同じインデックスを使おうとすると問題が発生します。再確保しないといけない場合もありますし、リザーブした領域を超えるとエラーが発生します。普通の文字列データでは、インデックスを別の変数で持てば良いのですが、MSStringのコレクション操作については、特別な考慮が必要です。
Swiftでは、UTF-16.view
やUTF-8.view
のように、文字列のエンコーディングに応じたビューを取ることができます。このビューでインデックス操作をする際に、特定のインデックスが存在しない場合があります。最近の仕様変更で、アンバウンドエンジンというエラーが出ることがありますが、これは基礎的な部分のインデックスサポートが存在しないためです。
サブスクリプトには特別な構文があり、ラベルをつけることも可能です。これにより、例えば「セーフインデックスアクセス」を作成して、安全に使えるインデックス操作を提供することができます。ラベルをつけることで、ユーザーが意図した動きを明確にすることが可能です。
UTF-16.view
やUTF-8.view
のインデックス操作については、イント型のインデックスが使えない場合もありますが、ユニコードスカラービューではイント型のインデックスが使えそうです。ただし、ユニコードスカラー
の意味を理解する必要があります。
最後に、ストリング型では1文字が何バイトで表現されるかが不定であるため、特定の文字位置を操作する際には注意が必要です。次の文字が何バイト目にあるかは、文字セットに依存するので、正確にインデックスを管理するためには、ストリング変数を使用する必要があります。 なので、必ずStringを母体にして、インデックスが何番目かを調べる際には、StringのインデックスやStringのスタートインデックスを基準にして操作する必要があります。こういう形式を取るようになっています。
昔はもっと単純に書けたんですよ。インデックス型が親の情報を持っていて、インデックスが次のインデックスがどこから始まるかという情報を持っていて、自然な感じに書けていたんです。しかし、いろいろな理由でインデックスの基準となる情報をインデックス型に持たせないことで、責任範囲の整理といった面で問題点を解決したようです。ですが、これによって、うっかり同じ母体であるべきところを取り違えるとおかしなことになる可能性があります。ちょっと残念というか、まあ大げさかもしれませんが、こういうデメリットがあります。
確かに、Stringの安全性を優先した結果として、使い勝手が少し損なわれている感じがあります。コメントでもいただいた通り、書くたびにちょっとまどろっこしいんですよね。ですので、もしかするとこのあたりをもっと洗練させた表現というのが発想次第で出てくるかもしれません。Swift APIデザインガイドラインからしても強調されている表現となっており、過不足なく表現するのが理想されているにも関わらず、過剰な表現になってしまっていて、自然な英語としては読みにくい状況になっています。
こういったところの理想の表現を追求するのが好きな方は、発想を練っていくことで、次世代の書き方(今はモダンですが、未来のモダンな書き方)を発見できるポテンシャルがあるかもしれません。興味がある方は研究してみても良いかもしれないし、世の中に大きく貢献するかもしれません。まあ大げさかもしれませんが、意外と重要なポイントだと感じます。
UTF-8やUTF-16の場合、UTF-8なら次のバイトは1バイト後、UTF-16なら2バイト後というわけです。昔はInt
型でインデックスアクセスしていたと思いますが、今はちょっと変わっていますね。まあ、頭を使わずにInt
型を使っちゃえば良いのですが、UTF-16オフセットというものがあり、StringインデックスをUTF-16として移動させるのが少し怖い気がしますが、どうでしょうか。
個人的にはそのビューを飛び越えちゃダメでしょうと思いますが、面白いですよね。こういったところが楽しいところでもあります。では、スライドに話を戻していきます。
配列について個人的に面白いと感じたのは、最後の要素にカンマがあっても良いという点です。他の言語でも一般的にありますが、Swiftでもこれを採用するのがとても面白いと思います。
例えば、let modes = ["Standard", "Release", "Debug",]
という風に、配列の最後の要素の後にカンマをつけていても許容されます。これにより、後で要素を追加する際にエラーが発生しないようにできます。カンマを入れておくことで、次に要素を追加しても問題なくコンパイルが通るという利点があります。
これがSwiftにも採用されているのです。自分がこの書き方に初めて出会ったのはSwift以外の言語でしたが、その時は気持ち悪く感じていました。しかし、Swiftでもこれが採用されると、自分も意見が変わり、納得できるようになりました。そんな感じで捉えています。 まあ便利ですよね。価値観が変わっちゃったところなんですけど、これは実際によく使ってますかね。パッケージマネージャーで標準で使われてますし、テンプレートとかでも使われてますよね。好きな人はきっと好きでしょう。でも、やっぱり一生懸命消している人もいますね。言語構文に特に意識の強い人は消しているような気がします。
そういう人でも、当然仕様として許されているので積極的に使って問題ないです。これが悪影響を及ぼすことはないので、逆にいいことしか起こさないでしょう。積極的に使っていいと思います。もちろん、カンマが並んだらエラーになりますしね。これでエラーになりますよね。なるよね、そうそう。こんな風にね、最後だけ特別なルールなので積極的に使っていきましょう。
これはダメですね。ダメですね。何か変なエラーになった?ああ大丈夫です。これは個人的にすごく些細なところなんですけど、感動したポイントなので紹介しておきたいですね。この例、スライドの例は辞書リテラルでやってますよね。自分がやった例は配列リテラル。このどちらでも使えますので、ぜひ積極的に使っていくといいと思います。ただ、関数の引数にはこれできないんですよね。
例えば func summarizing(a: Int)
というのは普通ですよね。でもカンマで終わらせることはできません。ここはダメです。引数が多くなってきたときに分けることもあるじゃないですか。今時はやらないのかな?この感覚で同じようにやりたくなりますけど、やらないですけどね。ここはダメなんです。面白いですよね。自分にはちぐはぐ感を感じるけど、まあ全然違うんでしょうね。
次に行きましょう。はい、次。配列の自動拡張。配列は要素を加えると自動的に拡張されます。要は動的配列ですよという話です。append
メソッドを使って加えることができます。この話は前回の勉強会でお話ししたので、そちらを見れば詳しくわかるでしょう。そんなに難しいところではないと思うので、これくらいでいいかな。
配列の動的アクセスについて。これくらいでいい気もしますけど、話そうと思えば色々話せることはあります。動的配列の仕組みについての詳細は次に行きますかね。そして、空の配列や空の辞書を作るとき、空のリテラルを使った初期化式が使えます。これも前回の勉強会で徹底的に話したところなので、そちらを参考にしてください。
例えば、配列リテラルに中身を何も書かないだけで空の配列が作れますし、辞書リテラルにも中身を書かないで空の辞書が作れます。型がリテラルだけからは推察できないので、型を添えてあげる必要がありますが、文脈から型が特定できる場合は型を添えなくても使えます。この型推論の話も前回お話ししましたので、詳しくはそちらを見てください。
この後は辞書や配列の話は終わりますが、次のセクションに行くには時間が足りないので、もう少し配列や辞書の仕組みのところを細かく見ておきましょうか。補足的なところですが、どういう風に作られているかを見てみましょう。特に Array
とかを見たらわかりやすいかな。色々複雑な構造になっていますからね。 まず、その概要をおさらいしておきましょう。型を作る際、また配列を作成する上で重要となるプロトコルには「シーケンス」と「コレクション」が挙げられます。
シーケンスとは、要素が連続していることを表現するプロトコルで、ある要素の次に次の要素が続き、その次の要素が続いていくということを示します。シーケンスが持つ情報としては、最初の要素から次の要素、その次の要素が取得できなくなるまで続きます。これを実現するために、イテレータープロトコルというものが内包されていて、次の要素が何であるかという情報を取得する役割を持っています。
シーケンスの重要なポイントとして、「次へ次へとしか進めない」という点があります。これをもう少し自由にアクセスできるようにするための仕組みが「コレクション」です。コレクションの重要な特徴はインデックスアクセスが可能であることです。このインデックスアクセスを実現するためにサブスクリプトが用意されています。
コレクションプロトコルに準拠すればインデックスアクセスが可能であり、さらにコレクションはシーケンスプロトコルにも準拠しているため、インデックスを最初から順にたどっていくとシーケンスとしての性質も持っていることがわかります。つまり、コレクションは要素を連続して持っておけるというシーケンスの性格も併せ持っているわけです。
また、コレクションの内部の値を書き換え可能にしたい場合、それはミュータブルコレクションに準拠する必要があります。これによって内部の要素を変更することができます。要素を追加するためのインターフェースはレンジリプレイシングダブルコレクションで規定されています。こうした複数のプロトコルを使って配列が表現される仕組みになっているのです。
ディクショナリーも同様です。ディクショナリーやアレイについて理解を深めたい場合には、コレクション系のプロトコルやその機能について調べていくことで、理解が深まります。
コレクションを最初に見ると、型パラメーターとしてエレメントを取ることがわかります。エクステンションのアレイなどを調べていくと、範囲でサブアレイをリプレイスできる性格を持っていること、カスタムリフレクタブルによってリフレクションする際に使われること、またカスタムストリングコンバーティブルによってアレイを文字列で表現するテキスト表現があるなどの機能がわかります。
なお、ランダムアクセスコレクションについても紹介し忘れていましたが、その機能も理解の一助となるでしょう。 これはどのようなインデックスを投げてもオーダー1、つまり一定の速度でレスポンスが返ってくる、ランダムアクセスという方式です。他に明確な言葉はありませんが、シーケンシャルアクセスという言葉もITの用語として存在しています。これは先頭からしか取り出せないという重要な特性を持っています。
シーケンシャルアクセスとランダムアクセス、この2つの言葉は対で覚えておくと理解が深まります。昔話になりますが、確かファミリーコンピュータのディスクシステムはシーケンシャルアクセスでした。また、ストレージとしてカセットテープを使っていたころもシーケンシャルアクセスでした。途中からアクセスすることが可能ではありましたが、基本的には難しいシーケンシャルアクセスだったのがカセットテープの実機記憶デバイスの時代です。
それに対してフロッピーディスクはランダムアクセスが可能な実機ディスクシステムという違いがあり、アクセス速度が圧倒的に異なります。アクセス時のあれこれを工夫すれば、実機ディスクでもランダムアクセスは技術的に可能ですが、人間の肌感覚としてはどうするか。なるべくランダムアクセスを実装することでパフォーマンス向上に繋がります。
配列はランダムアクセスになっているため、インデックスアクセスが高速です。さらに、ミュータブルコレクションなので配列の要素の内容を読み書き可能です。しかし、要素の追加や削除に関しては、レンジリプレイサブルコレクションにより可能であることがわかります。
エレメントが String
プロトコルだった場合の特別な配慮があるのか、次に、エレメントがシーケンスだったときにも特別な配慮があります。この例を見れば、具体的な実装が参考になるでしょう。エレメントが String
である場合など、細かく定義されていると汎用的なコードが書けるようになります。それは、パフォーマンス向上のためです。
例えば、要素が比較可能であれば、配列全体も比較可能にするといった具合に特化させています。アレイ型は非常に規模が大きなもので、パフォーマンス向上が図られています。そのため、安心して様々な場面で利用できます。これはここに書かれていませんが、気持ちの表れがここに出ています。配列にはコピーオンライトなどの工夫がされており、仕上がっています。
ディクショナリーも同様に見ていけばOKです。こうやって理解を深めることで、視野が広がるので興味があればどうぞ試してみてください。
それでは時間もあと50秒ほどになりましたので、今日の勉強会はこれで終わりにしようと思います。何か質問はありますか?大丈夫そうですね。基本的なところを押さえましたので、難しいところも特になかったでしょう。
では、今日の勉強会はこれで終わりにしましょう。1時間お疲れ様でした。ありがとうございました。