Swiftも1.0がリリースされ、SwiftでiOSアプリを申請できるようになりました。Swiftはとても学びやすい言語ですが、 Array (や Dictionary )の挙動については最初戸惑う人が多いのではないかと思います。そこで、Swiftを学習するときに Array についてハマりそうな点をまとめました。
多くの言語では配列やリストは 参照型(Reference Type) として扱われますが、 Swiftの Array は 値型(Value Type) です。配列やリストが参照型である言語(Objective-C、C#、Java、JavaScript、Python、Ruby、…)に慣れ親しんでいる人ほど、この違いに戸惑うのではないかと思います。
本稿では Arrayが値型であることによってハマりがちなポイントについてまとめます 。個別のメソッドの使い方などでハマりがちな点については対象としませんのであしからず。
なお、ここでは取り上げませんが Dictionary については全く同じことが言えます。
同じArrayを参照しているとかんちがいする
Swiftの Array は次のような挙動を示します。
var a = [1, 2, 3]
var b = a
b[0] = 999 // a[0]は999ではなく1のまま
多くの言語の配列では a[0] も 999 になります。そのような配列に慣れているとSwiftの Array の挙動は不自然に思えるかもしれません。しかし、それは参照型の配列に慣れすぎただけです。本来は、参照を共有している状態の方が不自然なはずです(詳細は後述)。
Swiftの Array は値型なので 代入時には Array が丸ごとコピー されます。 b = a の時点で Array はコピーされるので、その後で b を変更しても a には何も影響を与えません。
Arrayがvarとletでミュータブルやイミュータブルに変化すると考える
Swiftの Array は varで宣言するとミュータブル(可変)に、letで宣言するとイミュータブル(不変) になります。
var a = [1, 2, 3]
a[0] = 999 // OK
let b = [1, 2, 3]
b[0] = 999 // コンパイルエラー!
では、 let で宣言したイミュータブルな Array を var に代入して変更するとどうなるでしょう?
let a = [1, 2, 3]
var b = a
b[0] = 999
これについても前節のように、 b = a の時点で Array はコピーされいるのでその後 b に変更を加えても a には関係ありません。 a[0] は 1 のままです。 var か let かでオブジェクトがミュータブルかイミュータブルかが変化しているわけではない のです。 b は a から要素をコピーして作られた、 a とは別のミュータブルな Array になります。
var で宣言した Array を let に代入した場合も同様です。
var a = [1, 2, 3]
let b = a
a[0] = 999 // b[0]は1のまま
Arrayを引数に渡して変更しようとする
Array を関数やメソッドに渡す場合も注意が必要です。
// Arrayの要素をすべて2倍する関数
func makeDouble(var array: [Int]) {
for var i = 0; i < array.count; i++ {
array[i] *= 2
}
}
var a = [1, 2, 3]
makeDouble(a)
println(a) // [1, 2, 3]のまま
上記の関数 makeDouble は参照型の配列にとってはごく自然な実装ですが、Swiftの Array では正しく動きません。
Array は値型なので、 引数に渡される場合もコピー されます。コピーされた Array を変更してもそれは元の引数に渡された Array とは関係ありません。 Int を引数に渡して、それを関数の中で変更しても元の値は変更されないのと同じです。
関数やメソッドにArrayを渡して変更する方法
では、関数やメソッドに渡された Array を変更するにはどう書けば良いでしょうか。
Swiftでは、 引数で渡された値に変更を加える場合は inout という特別なキーワードを使います。
// Arrayの要素をすべて2倍する関数
func makeDouble(inout array: [Int]) { // inoutを付ける
for var i = 0; i < array.count; i++ {
array[i] *= 2
}
}
var a = [1, 2, 3]
makeDouble(&a) // &をつける
println(a) // [2, 4, 6]
inout が付与された引数は、関数・メソッドの中で変更された場合にその変更が呼び出し元に反映されます。また、 inout な引数を持った関数・メソッドをコールするときには値に & をつける必要があります。これはC言語でポインタを渡すときと同じなのでC/C++経験者には親しみやすいと思います。 inout は値の参照をとって関数・メソッドに渡しているようなイメージだととらえると、 Array を渡して変更できる意味がわかりやすいと思います。
ただし、 inout で渡された値であっても関数の中からはただの値にしか見えませんし、参照として保持しておくこともできません。そのため、 inout を使っても Array の参照をとって共有することはできません 。
どうしても Array を共有したければ、 Array をラップしたクラスを作ってそのインスタンスを共有することはできます。参照型の配列に慣れているとそういうことを考えてしまいがちですが、よくよく考えてみると Array そのものを共有しなければならないケースはほとんどない と思います(詳細は後述)。
オブジェクトの内外でArrayを共有しようとする
クラスに Array を保持するにも、参照型の配列とは勝手が違います。
Array を保持するクラスの例として、SNSのようなサービスを考えて、ユーザーを表す User クラスと、ユーザーが所属する Group クラスを考えてみましょう。 Group クラスは所属する User の Array を持ちます。
class User{}
class Group{
var users: [User]
init(users: [User]) {
self.users = users
}
}
var users = [User(), User(), User()]
let group = Group(users: users)
users.append(User()) // group.usersには追加されない
このようにオブジェクトに渡した Array を後から変更しても、オブジェクトの保持している Array には影響を与えません。これも、 イニシャライザに渡すときに Array がコピーされる からです。イニシャライザではなく、 プロパティやメソッドに Array を渡す場合も同様 です。
また、 オブジェクトから受け取った Array を変更した場合も、 return および代入時にコピーされるのでオブジェクト内の Array には影響を与えません 。
var groupUsers = group.users
groupUsers.append(User()) // group.usersには追加されない
余談ですが、G のコード中で group は let で宣言されていますが、代入された Group オブジェクトがイミュータブルになるわけではない ことに注意して下さい。 Group は Array とは違いクラスです。 let でも group に格納されたオブジェクトへの参照が不変になる( group に再代入できなくなる)だけ で、オブジェクトが不変になるわけではありません。 let はJavaで変数宣言に final をつけているのと同じです。
オブジェクトが保持しているArrayの変更する方法
では、オブジェクトが保持している Array を変更するにはどうすれば良いでしょう?例えば、次のようにします。
group.users.append(User()) // group.usersに追加される
あるいは、 Group クラスにユーザーを追加するメソッドを実装しても良いでしょう。
class Group{
var users: [User]
init(users: [User]) {
self.users = users
}
// ユーザーを追加するメソッド
func appendUser(user: User) {
users.append(user)
}
}
var users = [User(), User(), User()]
let group = Group(users: users)
group.appendUser(User()) // group.usersに追加される
オブジェクトの内外でArrayを共有する方法
どうしても Array を共有したければ、前述のように、 Array をラップしたクラスを作ってそのオブジェクトを共有することで、間接的に Array を共有することができます。
しかし、よく考えてみて下さい。本当にオブジェクトの内外や複数のオブジェクト間で Array と Array への変更を共有したいようなケースがあるでしょうか。 カプセル化されたオブジェクトの内部状態をメソッドを介さずに変更できるのは望ましい設計ではありません 。
たとえオブジェクトの内外で共有しなくても、 Array を複数の変数に格納して、片方を変更するともう片方も変更されてうれしいのはどんなケースでしょうか。単に、参照型の配列に慣れすぎて、Swiftの Array の挙動が不自然に感じているだけではないでしょうか。 Array を共有したいという気持ちになったときは、設計に問題がないか振り返ってみるのがいいと思います。(こんなケースではどうしても Array を共有しなければらないというケースがあれば教えてもらえるとうれしいです。)
パフォーマンスのためにArrayのコピーを避けようとする
そもそも、多くの言語ではなぜ配列は参照型として実装され、オブジェクトが共有されるのでしょうか。それは、代入の度に配列が丸ごとコピーされたのではパフォーマンスが悪すぎて問題だからです。すると、Swiftでもパフォーマンスのために Array のコピーを避ける(例えば、前述のように Array をラップしたクラスを作ってそのインスタンスを共有する)必要があるのでしょうか。
結論から言うと、Swiftでは Array のコピーによるパフォーマンスへの影響を気にする必要はほぼありません。
確かに、概念的にはこれまで見てきた通り、
- 代入されるとき
- 引数に渡されるとき
- 戻り値として return されるとき
のすべてで Array はコピーされます。プログラムの実行結果はそのように考えた通りになります。
しかし、いくらなんでも本当に上記のケースで毎回 Array をコピーしていたら、パフォーマンスが悪すぎて使い物になりません。 Swiftの Array は不要なコピーを行わないように最適化されており、実際にはコピーはほぼ実行されません 。結果が変わらない範囲でコピーをさぼって、コピーしないと不都合が起こるときだけコピーするのです。
具体例を挙げます。次のコードの1〜4のどこでコピーが発生するでしょう?
// 100万個の要素を持つArray
var a: [Int] = ([Int])(1...1000000)
var b = a // 1
println(b[0]) // 2
a[0] = 888 // 3
a[0] = 999 // 4
Swiftの Array は値型ですが、内部では実体への参照を保持しています。 Array を代入すると実体への参照だけがコピーされ、不都合が生じるまでは実体が共有されます。 K の例では次のようになります。
- 代入したが、この時点ではコピーせずに実体を共有
- b[0] を取り出すだけなら a と b で同じ実体を共有していても問題なし
- a[0] が変更され a と b は異なる Array である必要が生じたのでここで実体をコピー
- 再度 a を変更しても、 a の実体は誰とも共有されていないのでコピーは不要
同じように、関数やメソッド、イニシャライザの引数に渡してもコピーは発生しません。
func sum(numbers: [Int]) -> Int {
var sum = 0
for number in numbers {
sum += number
}
return sum
}
println(sum(a)) // aの実体を共有しても問題ないのでコピーは発生しない
関数の戻り値として Array を return してもコピーは発生しません。
// 与えられた値までのArrayを作成する関数
func numbers(count: Int) -> [Int] {
return ([Int])(1...count)
}
var c = numbers(1000000) // returnして代入してもコピーは発生しない
c.append(1000001) // 変更してもコピーは発生しない
上記のコードでは、 return された Array に変更を加えていますが、その時点で関数のスコープは抜けているため、この Array の実体は他から参照されていません。そのためやはりコピーは発生しません。
このように、 Array は複数箇所から実体を共有されない限り実際にコピーされることはありません。よほどおかしな設計でなければ不要にコピーが発生してパフォーマンスが劣化するようなことはないでしょう。Swiftにおいては、 __コピーを避けるために Array を共有しようとするのは不自然でわかりづらいコードに生むだけ__でメリットはないと思います。
Array のコピーについてより詳しくはこちらをご覧下さい。
CovariantなArrayが型安全性を破壊していると思う
ハマりどころとは少し違いますが、 Array が値型であることに起因した挙動なので挙げておきます。
一見おかしなように感じますが、Swiftの Array はミュータブルだけどCovariantです。それでいて型安全です。詳しくはこちらをご覧下さい。
まとめ
多くの言語と違いSwiftの Array は値型であるため、配列が参照型である言語に慣れ親しんだ人にとって直観と異なる挙動を示します。そのことによってハマりがちなポイントをまとめました。
最初は不自然に思えるかもしれませんが、 Array が値型であるということを理解してしまえば一貫性を持ったシンプルな挙動であることがわかりますし、むしろ Array は値型である方が自然で望ましいのではないかと思えてきます。