https://www.youtube.com/watch?v=q-adbWB2zFU
今回は A Swift Tour
の「配列と辞書」についてみていきます。
Swift に限らない多くの言語で馴染みの深いところですので、基礎的な動きを確認してから、それらが Swift ではどのような構造で表現されているかみたいなところも眺めてみようと思ってます。よろしくお願いしますね。
——————————————————————— 熊谷さんのやさしい Swift 勉強会 #30
00:00 開始 00:59 配列 (Array) 01:50 辞書 (Dictionary) 02:46 配列と辞書の基本 05:45 インデックスアクセス 06:42 配列と辞書の型 07:49 糖衣構文 09:57 どちらの表記方法を使うか 20:10 配列型と配列リテラル 22:33 コレクションのリテラル表記 27:01 配列や辞書は値型 31:39 イミュータブルクラス 34:35 変数と定数の両方で使える型 41:32 値型の配列を扱う際の注意 44:41 Copy-On-Write 45:57 配列型のサイズ 48:34 内部でバッファーへの参照を持つ 50:53 次回の展望 52:09 さまざまな配列 52:59 クロージング ———————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #30
今日のテーマは配列と辞書になります。いよいよ、ちゃんとしたSwift言語の勉強に入っていく感じですね。このセクションでは、特に配列についてじっくりと見ていこうと思います。
まず、配列について説明します。プログラムをやっている人なら普通にイメージできるものだと思いますが、一応説明しますね。配列というのは、ある要素が一箇所にまとめられているものです。Swift的に言うと、値をコレクションする変数で、インデックスアクセスによって値を取り出します。この仕組みを今日は具体的に見ていこうと思います。
また、今日一緒に見ていくものとして辞書(ディクショナリー)もあります。辞書というとイメージしにくいかもしれませんが、プログラムの世界ではディクショナリーやハッシュテーブルといった言葉でよく知られています。要は、キーとバリューのペアで値を管理するものです。どの言語にも配列や辞書に相当するものが含まれていますので、すでにイメージできる人も多いと思います。今回の目的は、これらの配列や辞書に慣れていくことです。
では、実際にプレイグラウンドで見ていったほうが早いかもしれませんが、一応基本的なところを少し説明しておきますね。アクセスする際には、括弧を使ってインデックスやキーを添えて表現します。配列はSwiftの場合、整数型のインデックスを使用し、ゼロから始まるインデックスでアクセスします。辞書の場合は、数字ではなく文字列などのキーを使ってアクセスします。
やはり実際にコードを書いてみたほうが分かりやすいので、プレイグラウンドで見ていきましょう。まず配列の基本的な例として、以下のようなコードを書いてみます。
let values = [1, 2, 3]
また、別の例として価格を示す配列を作ります。
let prices = [100, 120, 80]
辞書の方も見てみましょう。例えば、果物の価格を示す辞書を次のように作ります。
let fruitPrices = ["Apple": 100, "Banana": 120, "Cherry": 80]
配列にアクセスする際は、インデックスを利用します。例えば、prices
の2番目の要素にアクセスしたい場合は以下のようにします。
let secondPrice = prices[1] // 結果は120
辞書の場合は、キーを利用してアクセスします。
let bananaPrice = fruitPrices["Banana"] // 結果は120
ここまで基本的なアクセス方法です。では、Swiftでこれらがどんな型になっているかを見てみましょう。type(of:)
関数を使って調べることができます。
print(type(of: values)) // Array<Int>
print(type(of: fruitPrices)) // Dictionary<String, Int>
これらの基本的な型情報は、次のように明示的に書くこともできます。
let arrayOfIntegers: [Int] = [1, 2, 3]
let dictionaryOfStringToInt: [String: Int] = ["Apple": 100, "Banana": 120, "Cherry": 80]
ただし、辞書や配列は頻繁に登場するため、簡単に書ける仕組みが用意されています。次のように書くことで、同じ型を簡単に表現できます。
let quickerArray: [Int] = [1, 2, 3]
let quickerDictionary: [String: Int] = ["Apple": 100, "Banana": 120, "Cherry": 80]
このように、簡潔な表記もSwiftではサポートされています。型について詳しく調べたい場合、インスタンスに対してドット表記を使って機能を把握する方法もありますし、型定義をたどる方法もあります。
例えば、Xcodeのナビゲートメニューから"Jump to Definition"を利用して型定義へ飛ぶことができます。また、Control-Command-J
を使ってショートカットを実行することもできます。個人的には詳細な型情報を見るために11行目のような完全な型を指定する書き方を使うことが多いです。
これで配列と辞書の基本的な使い方の紹介は終わりです。次に進む前に、これまでの内容を念頭に置きながらコーディングしてみてください。 なので、積極的に書いたりしますけど、どっちがいいんでしょうね。どっちも読みやすさは一長一短かなっていう気がして、自分の中の評価では、10行目も11行目もどっちもどっちです。なんかこっちのほうがちょっと良いよねとも思わないくらいに対等に見えていて、いつも選ぶのに困っています。一般的にはどうしているんでしょうね。まあ、10行目ですかね。
そうですね、10行目も11行目も使わないことはないですよね。
そうですね、ほぼないですね。書き方のときも違うし、短い方がいいですよね。これは割と一般的な考えなので。
こうやってインスタンス化する時のイメージはどうなんですかね。11行目は明らかにイニシャライザーを読んでいるじゃないですか。でもやっぱ10行目ですよね。
そうですね、10行目にしますね。言われてみれば、わかりやすいかどうかというと、覚えているから読めるっていうだけな気もします。まあ、一応統一公文(syntactic sugar)として提供してるところも踏まえると、10行目を使っていったらいいよねっていう感覚な気がしますね。
ああ、そうですね。10行目も11行目も書いてみますけど、普通に中にその...1行目みたいな感じでやるなぁと思ったけど、から入ると確かに、こういう書き方もありますね。例えば、let
あとは型を書いて、その間に書く方がね。
そうですね。この10行目から15行目まで全部同じコードになりますが、どれがお好みなんだろう。短い方がいいですね。10行目が一番短いですね。並べて書いてみるとやっぱり10行目か14行目が安心する気がします。見慣れてる感じはありますよね。
はい、そうですね。こうやっていろいろな書き方があるのを見ていくのも面白いですよね。番外編として今コメントもらいましたけど、配列としてリピーティング(repeating)っていうイニシャライザーがあるわけですけど、それを使うときだけこの書き方をするってコメントを寄せてくれた方もいます。
まあ、これは妥当ですね。こうする以外にないですよね。例えば、やったことないですけどリピーティングも書けるんですね。面白いですね。本当に動くんですかって信じられないけど。
うん、ほんとに単純に期間してくれたけど、あ、でもエラーって言った、これはString
って書いちゃったからですね。ほんと単純に10と11の期間の違いだけなんですかね。
そうかもしれないですね。やっぱり統一公文ってもともとそういうものなんです。パッと勝手に置き換えてくれるという特別なことではなく。
18行目、多分見慣れてないだけなんでしょうけど、気持ち悪いですね。見たことないし。でも同じ発想で言ったら、18行目がいいんじゃないって言ってくれないとおかしいんじゃない?
まあ、10行目と18行目自体同じような使い方をしてるんじゃないか。カラー配列だけの特別な感じがありますよね。
そうですね、カラー。でもパラメーター取るときとか、別にカラーじゃないけど使ってますよね。
ありえますね。そうですね、こっちですね。この17行目っていうのは。で、リピートしたら、このString
をイコールinit
で書くんですかね。自分は知らなかったですが、面白いですね。こうやって眺めてみればね。
パッと見たときに、どれがいいとか、どの文脈だとどれがいいとか全然違ってくるので、意外と今回のこのお話は不毛かもしれないですけど、それでもいろんな書き方ができるってことを知っておくのは悪いことではないので、参考にしてもらえればいいかなと思います。いや個人的には18行目が面白かったな。
どっからこの話になったんだっけ、あ、そっか。統一公文ですね。この書き方が統一公文です。
あと、この統一公文を使うと、人によってはもしかすると混乱するかもしれないんですけど、こちらは配列リテラルという書き方になります。
let numbers = [1, 2, 3, 4, 5]
このように、配列リテラルを書くことで、簡潔に初期値が設定できます。 ここでは辞書型のリテラルについて説明します。辞書リテラルというのは、鍵と値のペアで構成されるデータ構造を簡単に表記できるものです。構文が似ていますが、リテラルは具体的な値(インスタンス)を扱うのに対し、通常の型定義は要素の型を指します。この違いを把握しておくと、見た目の混乱が減るでしょう。
そもそも多くの方はもう慣れているかもしれませんが、辞書リテラルも配列リテラルと同じように鍵と値のインスタンスを各カッコで括るという点が重要です。対して、通常の型定義ではインスタンスではなく型そのものを括ります。
重要なポイントとして、配列リテラルは各カッコ []
で、辞書リテラルも同じく []
で表記し、その間に鍵と値をコロン :
で区切ります。例えば以下のようになります:
let emptyArray: [Int] = []
let emptyDictionary: [String: Int] = [:]
これが空の配列および空の辞書の表現です。空のリテラルは型の推論が難しいので、型を明示する必要があります。
Swiftでは、配列や辞書など複数の要素をまとめるものを「コレクション」と呼びます。このコレクションのリテラルは必ず各カッコ []
で表現される点が、個人的には気に入っています。JavaScriptの場合は配列は []
、オブジェクト(辞書)は {}
となり、言語ごとに記号が異なるため、混乱を招きやすいです。Perl だとさらに複雑で、丸カッコ ()
で配列もハッシュテーブル(辞書)も表現するなど、記号の使い分けが学習コストを高くしています。
Swiftの場合、各カッコ []
さえ覚えておけばよく、記号の統一感もあるため非常に使いやすいと感じます。
次に、Swiftの重要な特徴として、配列や辞書は値型であるという点があります。例えば以下のコードを考えてみましょう:
var arrayA = [0, 0, 0]
var arrayB = arrayA
arrayB.append(1)
この場合、arrayB
に新しい値を追加しても元の arrayA
には影響がありません。これは、Swiftでは配列や辞書が値型であり、代入時にその内容が複製されるためです。多くの言語では、このような大きなデータ構造は参照型として扱いますが、Swiftでは異なります。この点が混乱を招くことがあるかもしれませんが、理解しておくと役立つでしょう。
興味深い点として、配列や辞書などのコレクションを操作する場合、その操作の結果が元のコレクションに影響を与えない点です。これを利用して、安全で予測可能なコードを書くことができます。
このように、Swiftの基礎的な特性とその操作方法について理解を深めることができます。では、次に行くべきポイントとして、配列や辞書の具体的な操作の仕方や応用例を見ていくことにしましょう。 「なので、Bに何かを追加するとAにも追加されているように見えることが多かったりするんですけど、Swiftの場合はそういうことがありません。ここがとても大事なところです。Foundationでは違いましたよね、確か。例えば var b = NSArray(array: [0, 0, 0])
として、var c = b
のように設定すると、b
に値を追加すると c
も変わってしまいます。これは参照型だからです。
一方、Swiftでは値型を使うことで、こういった問題を解消しています。値型はそれぞれが独立したものになっているため、お互いが不要意に干渉しません。これはSwiftのとても重要なポイントです。
この考え方がなかった頃のObjective-Cの時代には、参照型として設計されていたため、例えば NSMutableArray
のようにミュータブル(可変)クラスを用いて対策をしていました。インスタンスを作ったらその値は基本的に書き換えられず、書き換えるためのインターフェースを提供しませんでした。値を保護しつつ、参照型の複製を取りたいときにはコピーを使います。この方法で値が分断される動きを制御していました。
コピーを呼ぶと、イミュータブルクラスの NSArray
が取れて、さらにミュータブルコピーすると NSMutableArray
が取れる、そういうふうに動きが変わっていました。プログラマーがこれを一生懸命制御しないといけなかったのです。
このコピーメソッドは高度なことを要求していて、ミュータブルクラス、イミュータブルクラスの性質を理解する必要がありますし、どんな恩恵が得られるのかを知る必要があります。値型と参照型の違いを理解しながらコードを書くことが難易度を上げる要因となっていました。このような設定は、初心者には難しく、熟練者がデバッグする際にも困難を極めます。
参照型しかなかった時代には、配列を設計するときに不意に書き換えられてしまうとバグが深刻になるため、イミュータブルクラスを使う発想が出てきました。それでもイミュータブルバリューとミュータブルバリューの2つのクラスを作り、相互にブリッジを用意する必要がありました。クラスの数が増えると、その分設計の負担も増します。
一方、Swiftは値型として配列を扱うため、こうした問題を効率よく回避することができます。例えば、以下のように記述します。
var b = [0, 0, 0]
var c = b
b.append(1)
print(b) // [0, 0, 0, 1]
print(c) // [0, 0, 0]
このように、b
と c
はそれぞれ独立した配列として存在し、お互いに干渉しません。これがSwiftの設計思想であり、とても強力な特徴です。」
このように、Swiftでは重要な点として、値型を使うことによって参照型の問題を回避しているのです。これはプログラマーにとって非常にありがたい特性であり、コードの安全性と可読性を大きく向上させます。 とにかく、こうやって複数のものを用意して、かつイミュータブルクラスの場合は初期化が終わったら値を書き換えられないようにしないといけません。ですので、private set
などを使用して、値を外から書き換えるインターフェースを封じるといった方法を取ります。そういうふうな書き方をするのです。
また、値をインクリメントしていく場合には、インクリメントメソッドは必ずミュータブルクラスにのみ実装します。一生懸命頑張って作っていき、これでいざバリューを使う際には「これはイミュータブルがいいよね」とか、「まず書き換えたいからミュータブルクラスで作るよね」という風になるわけです。
そして、バリューのインクリメントやイニシャライザーがちゃんとできていない場合、とりあえず初期値を0にするとか、便宜上イニシャライザーを用意してあげるといったことが必要になります。たとえば、何かしらの距離をかけてみるなどですね。「これでいいのかな、違ってるかな」と思うこともあるでしょう。デフォルトイニシャライザーをつけなければならないこともあります。
このようにして、どちらにも意識して設計を作り、それが動くようになれば良いわけです。ただし、いつまでもミュータブルなままにしておくのは怖いこともあります。例えば、バリューやメソッドを呼んだときに、返す値がミュータブルの方が安心といった状況もあります。
これを回避するために、値を作った後にリターンでミュータブルバリューへ型変換をするなど、さまざまな工夫が必要です。しかし、Swiftの場合は構造体(Struct)で一発でそれが作れるのです。バリューを定義して、例えばvar value: Int = 0
と初期値を設定します。イニシャライザーを書かなくていいですが、一応書いておきます。
変換イニシャライザーは、そもそも型が2つの役割で分かれていないので不要です。インクリメントを実装する場合は、値が書き換わるときだけ実装し、value += 1
のようにします。値が書き換えられるインターフェースとなるため、mutating
をつけます。これは値を書き換えられるとき、つまりミュータブルなときに限って使えるものです。
これで完成です。イミュータブルクラスとミュータブルクラスに相当するものがこれだけで完成します。あとは、それを受け入れる変数が定数のタイプであれば、mutating
系が使えなくなりイミュータブルとして機能し、逆に変数として扱う形を取るとmutating
の機能も公開され、値を書き換えていくことができます。
具体的には、以下のようになります:
struct Value {
var value: Int = 0
mutating func increment() {
value += 1
}
}
var v1 = Value()
v1.increment() // これはエラーにならない
let v2 = Value()
v2.increment() // これはエラーになります
また、値を別の変数にコピーして利用することもできます。例えば、let v3 = v1
とすると、v1
では使えなかったインクリメントが、v3
では再び使えるようになるのです。構造体の特徴の話に逸れましたが、配列型も構造体として定義されているため、ミュータブルとイミュータブルの性格が変数の性格によって変わってくるというのがSwiftの大きな特徴です。 これはビクショナリーについても全く同様ですね。同じように動いてくれます。この辺りがなかなか厄介なところで、今時の人は感じないと思うんですけど、オブジェクトがあってこれが配列を持っている場合、オブジェクトのインスタンスを取ってオブジェクトのアイテムズの値を見ると、当たり前ですが[1, 2, 5]
となっていますよね。
これに対して値を追加するときに、オブジェクトのアイテムズに対してappend
を使って、例えば4を入れたり、8を入れたり、10を入れたり、マイナス1を入れたりできます。こうやって追加されていくことは非常に当たり前にやりますよね。
ですが、これをもっと端的に書きたい場合、参照型の発想で書いてしまうと、オブジェクトからアイテムズを一旦受け取ってそれに対して編集をかけると、反映されません。理由としては、参照型ではないからです。この動きは非常に当たり前なのですが、こういったことが起こり得ます。自分も以前はこれをやらなくなりましたが、最初の頃は間違って手間取っていた気がします。特にプロパティが深いときなどですね。
毎回書くのが面倒だから一旦編集用に受け取ってしまおうとすると、おかしなことになることがあります。そうなんです。コメントの方にも色々書いてくれている話の中で、代入すると複製されることが大きなメモリ消費を招くため、パフォーマンスが気になる点があるわけです。しかし、Swiftではコピーオンライトという仕組みによって必要最小限のコピーで済むように最適化されています。
これはこの勉強会の第1回目の30分過ぎの後半で具体的に話しているので、コピーオンライトに興味があればそちらを見てみてください。今日は別のことを話したいので、そこはちょっと飛ばしますね。
もう一つ、コピーオンライトにも関連するところなんですが、配列の面白い特徴として、普通Swiftの構造体は必要なメモリ容量を型そのものが確保します。ちょっと言い方を変えると、インスタンスそのものに必要なメモリが割り当てられます。例えば、Int64
ではなく、分かりやすくするためにInt8
の値が2個あった場合は、2バイト必要ですよね。 そういう風にメモリも割り当てられるんですよ。これでストレージ型のサイズは2バイトになります。これがさらにもう1バイト増えたら、計3バイトになりますよね。このように、構造体の構成に応じてメモリのサイズが確定するんです。
Swiftの配列はappend
でどんどん追加していける動的配列の性格を持っているんですけど、これをSwiftでどういう風なメモリレイアウトを持っているかというと、例えばInt
型のサイズだと基本的には8バイトです。これをもうちょっと増やしたい場合、例えばInt64
型に変えてみると、ストレージサイズは大きくなり、24バイトになります。しかし、配列のストレージ型のサイズはどうなるかというと、ストレージのまま変わらないのも面白いところです。
これは何の大した話ではなく、第1回目の「コピーオンライト」のところでも話しましたが、ストラクトアレイは内部で値を持つためのバッファーを便宜的にクラスで持っているんですよ。クラスはメモリサイズとしてはポインタ、要は参照だけのサイズになるので、構造体としては8バイトです。ただし、クラムクラスが用意したヒープメモリの方は別に8バイトで済んでいるわけではありません。そのため、ストレージの配列のサイズはストレージサイズ×Nみたいなサイズ感になります。厳密にはもう少し違ってきますが、こういった感じで実際のサイズは外側の方からは測れない感じで存在しています。
少し余談ですが、このような特殊な構造がパフォーマンス向上の秘訣であり、コピーオンライトにおいて大事なポイントでもあります。掘り下げていくと様々な特徴を見せてくれる面白い型なので、Swiftの雰囲気をより掴みたいときは、この配列に注目するといろんな発見があると思われます。
次回、この配列をもう少し深く見ていくか、次のセクションへ進むかは次の金曜日までに考えます。配列型の定義は非常に複雑で、様々なプロトコルに準拠していたり、多くのメソッドを持っていたりと、実に面白い特性が伺えるので、興味があればぜひ見てみてください。
Javaでは、Collections.unmodifiableList
のように、オブジェクト指向の特性を活かし、理想的な形を作るためのクラスが多く存在します。Swiftでも、用途に応じた特徴を持つ配列があり、パフォーマンスや用途を意識すると色々と出てきます。例えば、Immutable/Mutableの概念は構造体がうまくクッションしてくれて、初心者にとって非常に入門しやすい言語になっていると感じます。
時間もいい具合になってきたので、今回の勉強会はこれで終了にします。ありがとうございました。