これは 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
には注目しています。みなさんも興味があれば色々調べてみて下さい。