はじめに
先日、「イミュータブルの嬉しさって何だっけ」という記事で、イミュータブルなコードのメリットを整理しました。
その記事では具体的なコードの実現方法には触れなかったので、それをこちらの記事で改めてまとめようと思います。
ちなみにコードはKotlinを使っていますが、考え方などは他の言語にも通用する部分は多いかと思います。
イミュータブルの実現方法
イミュータブル(immutable
)とは「不変(変更できない)」ことを指す言葉です。
「変更できない」にも色んなパターンがあるので、それらの実現方法を1つ1つ説明していきます。
参照をイミュータブルにする: val
とvar
Kotlinでは、変数宣言で使えるキーワードとしてval
とvar
があります。
- val: 再代入不可(イミュータブルな参照)
- var: 再代入可(ミュータブルな参照)
val num: Int = 20
num = 40 // コンパイルエラー
var moji: String = "Hello!"
moji = "GoodBye!" // ok
読み取り専用とイミュータブル
ただし、val
はオブジェクトの中身をイミュータブルにするものではないことに注意が必要です。
val sample = Sample()
sample.num = 100 // オブジェクトのプロパティはvarなので変更できる
class Sample {
var num: Int = 20
var moji: String = "Hello"
}
val list = mutableListOf(1, 2, 3)
list.add(4) // コレクションに追加できる
このように、val
だけだと参照を変えられない(再代入できない)だけで、中身は変えられます。ちなみにこのような状態はread-only
(読み取り専用)と表現され、immutable
とは区別されます。
中身まで含めて不変にしたければ、この後に説明する「イミュータブルなオブジェクト」「イミュータブルなコレクション」の考えも必要です。
イミュータブルなオブジェクト
オブジェクトをイミュータブルにする場合、以下の条件を満たす必要があります。
- 全てのプロパティがval
- プロパティの型自体がイミュータブルであること
-
Int
,Boolean
などプリミティブな値を表す型1 - String
- Listなど、後述するイミュータブルなコレクション
- など
-
- 継承などでプロパティの可変性を変更できないこと(e.g. finalなクラス)
- ちなみにKotlinのクラスはデフォルトで
final
- ちなみにKotlinのクラスはデフォルトで
data class
Kotlinのdata class
は、イミュータブルなクラスを扱うのに向いています。
data class User (
val id: Long,
val name: String,
)
このUser
クラスは「すべてのプロパティがval」であるなど、前述の条件を満たしています。そのうえで、data class
に標準で用意されているcopy()
関数を使えばインスタンスの複製も安全かつ簡潔に行えます。
val user = User(1001, "Alice")
val renamedUser = user.copy(name = "Bob")
これにより、「元のインスタンスを変える」のではなく「新しいインスタンスを生成する」設計を自然に表現できます。
イミュータブルなコレクション
Kotlinはコレクションにもイミュータブルとミュータブルがあります。
例として、ここではList
を使って説明しますが、Map
やSet
など他のコレクションも基本的な考え方は同じです。
val list: List<Int> = listOf(1, 2, 3)
list.add(4) // コンパイルエラー。Listという型に`add`という関数は無い
val list: MutableList<Int> = mutableListOf(1, 2, 3)
list.add(4) // ok
型名にMutable
と付いている方(MutableList
)がミュータブルなコレクションで、こちらにしか要素を変更する関数(add
など)はありません。
なおMutable
が付いていない方(List
)は完全にイミュータブルな訳ではなく、あくまで読み取り専用のアクセスのみが提供されている型です。
型のイミュータビリティと中身のイミュータビリティ
少しややこしいポイントとして、型がイミュータブルかどうかだけではなく、中身のオブジェクトが実際にイミュータブルかも重要です。
例えば下記のような関数があったとします。
fun getList(): List<String> {
val list = mutableListOf("a", "b")
return list // 戻り値の型はListでも中身はMutableList
}
MutableList
はList
を継承しているので、このような関数も問題なく定義できます。
で、何がややこしいかというと、この関数が返すインスタンスはミュータブルなので、ダウンキャストされるなどしてミュータブルな型として扱われると、戻り値がイミュータブルな型(List
)だったのに変更される可能性がある、という点です。
val list = getList()
if (list is MutableList) {
list.add("c") // 変更できてしまう
}
もちろん、こういったダウンキャストは推奨されない書き方なので、ちょっと無理矢理な印象があるかもしれませんが、JavaとKotlinのコードが混在している場合、Javaでよく使われるArrayList
などはミュータブルなので、こういった問題が起こるリスクは現実的なレベルであり得ます。
対処法
toList
という関数を呼び出すことで対処できます。
fun getList(): List<String> {
val list = mutableListOf("a", "b")
return list.toList() // 読み取り専用の新しいリストが返される
}
これにより、関数が返すインスタンスも読み取り専用になるため、前述のような問題を起こさなくなります。
おわりに
前回の記事では触れなかった、イミュータブルなコードの具体的な実現方法について整理してみました。
特にコレクションに関しては少々ややこしい部分もあるので、実際に書いて動かしながら挙動を把握するのが良いと思います。
-
Kotlinでは、
Int
やBoolean
はプリミティブ型でなくオブジェクト型です。そのため「プリミティブな値を表す型」と少し濁した書き方をしていますが、JVM上ではプリミティブ型に最適化されるため、プリミティブのような型として理解して頂いても概ね問題ありません。ただ、言語仕様としてはvalue type
(値型)と表現することが正確であることを申し添えておきます。 ↩