これは Mobile Act OSAKA #1 での発表した内容をまとめたものです。
iOS / Android 双方に関係あることとして、 Kotlin の List と Swift の Array を比べてみます。
一番大事なこと
最初に一番大事なことを言います。それは、 Kotlin の List は 参照型 だということです。コレクションが参照型なのは当たり前と思うかもしれませんが、 Swift の Array はなんと 値型 です。この違いがこれから説明するすべての違いを生む原因なので重要です。
ミュータビリティ
では実際にどんな違いがあるのか、まずはミュータビリティについてです。
Kotlin
Kotlin では List がミュータブルかイミュータブルかを型で表します。
val a: MutableList<Int> = mutableListOf(2, 3, 5)
a.add(7) // [2, 3, 5, 7]
val b: ImmutableList<Int> = immutableListOf(2, 3, 5)
val b2 = b.add(7) // b=[2, 3, 5], b2=[2, 3, 5, 7]
// `val` は Swift の `let` 相当
ミュータブルだとこのように新しい要素を add するとそのインスタンス自身に変更が加えられますが、イミュータブルだと当然インスタンスを変更することはできません。 add すると新しい要素が加えられた生成され返されます。
ちなみに、この ImmutableList はまだ Proposal の段階なので標準ライブラリには存在しません。将来的には追加されるんじゃないかと思います。
この二つのリストを抽象的にまとめて扱いたいことがあるので、第三の型として、これらのスーパータイプである List がほしくなります。
List <-+-- ImmutableList
|
+-- MutableList
List は MutableList や ImmutableList のスーパータイプなので、それらのインスタンスを List 型の変数に代入することができます。しかし、逆はできません。
val c: List<Int> = mutableListOf(2, 3, 5) // OK
val d: MutableList<Int> = c // NG
↓のように明示的に変換してやる必要があります。
val c: List<Int> = mutableListOf(2, 3, 5) // OK
// val d: MutableList<Int> = c // NG
val d: MutableList<Int> = c.toMutableList() // OK
MutableList と ImmutableList の間でも当然変換が必要です。
このように、 Kotlin ではミュータビリティを適切に扱おうとすると三つの型を使い分ける必要があります。
Swift
では Swift ではどうかと言うと Array 型一つだけを使います。 Array は値型なので、ミュータビリティの切り替えは var か let か、つまり変数か定数かによって行います。
var a: Array<Int> = [2, 3, 5]
a.append(7) // OK
let b: Array<Int> = [2, 3, 5]
b.append(7) // NG
// `let` は Kotlin の `val` 相当
var で宣言すればミュータブルなので append で要素を追加することができますが、 let にするとイミュータブルなので append がコンパイルエラーになります。
また、 Kotlin の場合と違い、 var だろうと let だろうと型の上では同じ Array<Int> 型なので、相互に代入することができます。
let c: Array<Int> = [2, 3, 5] // OK
var d: Array<Int> = c // OK
インスタンスの共有
次はインスタンスの共有についてです。
Kotlin
Kotlin の List は参照型なので↓のように a を b に代入してから a を変更すると b も変更されてしまいます。
val a: MutableList<Int> = mutableListOf(2, 3, 5)
val b: List<Int> = a
a.add(7)
println(b) // [2, 3, 5, 7]
これは、 a と b が同一のインスタンスを参照しているからです。
意図せずにインスタンスが共有されてしまうとバグを生みかねません。特に、コンストラクタやメソッドの引数に渡す場合や、メソッドの戻り値として返す場合など、インスタンスをまたぐときに問題を生みやすいので注意が必要です。
それを避けるためには明示的にコピーをする必要があります。
val a: MutableList<Int> = mutableListOf(2, 3, 5)
val b: List<Int> = a.toList() // Copy
a.add(7)
println(b) // [2, 3, 5, 7]
Swift
Swift では Array は値型なのでインスタンスが共有されることはありません。同じように、 a を b に代入してから a を変更しても b が変更されることはありません。
var a: Array<Int> = [2, 3, 5]
let b: Array<Int> = a
a.append(7)
print(b) // [2, 3, 5]
値型は代入の度にコピーされるので、コレクションのようにインスタンスが巨大だとコピーコストが気になりますが、 Swift の Array は Copy-on-Write という仕組みで無駄なコピーが発生しないようになっています。
たとえばこの例では代入のタイミングではなく append したときに Lazy にコピーされます。
var a: Array<Int> = [2, 3, 5]
let b: Array<Int> = a // No copy
a.append(7)
print(b) // [2, 3, 5]
これは参照型で先行してコピーコストを支払わなければならないのと対照的です。
変性
最後は変性( Variance )についてです。
Kotlin
Animal クラスを継承した Cat クラスがあるとします。
Cat の List は Animal の List のサブタイプになります。そのため、↓の cats を animals に代入することができます。
val cats: List<Cat> = listOf(Cat())
val animals: List<Animal> = cats // OK
このような性質を、 List は型パラメータ E について共変( Covariant )であると言います。これは ImmutableList でも同じです。
しかし、 MutableList では事情が異なります。もし、この cats を animals に代入できてしまうと、↓のように animals を通して cats に Dog を追加でき、タイプセーフティが破綻してしまいます。なので MutableList は共変ではなく非変( Invariant )です。
val cats: MutableList<Cat> = mutableListOf(Cat())
val animals: MutableList<Animal> = cats // NG
animals.add(Dog()) // !!
Swift
Swift はどうかというと、当然イミュータブルな Array は共変です。
let cats: Array<Cat> = [Cat()]
let animals: Array<Animal> = cats // OK
しかし、なんとミュータブルな Array でも cats を animals に代入可能です。
var cats: Array<Cat> = [Cat()]
var animals: Array<Animal> = cats // OK
これは、 Swift の Array が値型だからです。インスタンスが共有されることはないので、 animals に Dog を加えても cats は変更されません。
animals.append(Dog()) // OK
まとめ
Kotlin の List と Swift の Array では挙動が色々と異なります。この違いを知らずに単純移植しようとすると大怪我をしかねないので気を付けて下さい。
個人的には、値型のコレクションはめずらしくて挙動がおもしろいので Swift の Array には注目しています。みなさんも興味があれば色々調べてみて下さい。