1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kotlinでのイミュータブルなコードの書き方

Posted at

はじめに

先日、「イミュータブルの嬉しさって何だっけ」という記事で、イミュータブルなコードのメリットを整理しました。

その記事では具体的なコードの実現方法には触れなかったので、それをこちらの記事で改めてまとめようと思います。

ちなみにコードはKotlinを使っていますが、考え方などは他の言語にも通用する部分は多いかと思います。

イミュータブルの実現方法

イミュータブルimmutable)とは「不変(変更できない)」ことを指す言葉です。
「変更できない」にも色んなパターンがあるので、それらの実現方法を1つ1つ説明していきます。

参照をイミュータブルにする: valvar

Kotlinでは、変数宣言で使えるキーワードとしてvalvarがあります。

  • 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

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を使って説明しますが、MapSetなど他のコレクションも基本的な考え方は同じです。

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
}

MutableListListを継承しているので、このような関数も問題なく定義できます。

で、何がややこしいかというと、この関数が返すインスタンスはミュータブルなので、ダウンキャストされるなどしてミュータブルな型として扱われると、戻り値がイミュータブルな型(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()  // 読み取り専用の新しいリストが返される
}

これにより、関数が返すインスタンスも読み取り専用になるため、前述のような問題を起こさなくなります。

おわりに

前回の記事では触れなかった、イミュータブルなコードの具体的な実現方法について整理してみました。

特にコレクションに関しては少々ややこしい部分もあるので、実際に書いて動かしながら挙動を把握するのが良いと思います。

  1. Kotlinでは、IntBooleanはプリミティブ型でなくオブジェクト型です。そのため「プリミティブな値を表す型」と少し濁した書き方をしていますが、JVM上ではプリミティブ型に最適化されるため、プリミティブのような型として理解して頂いても概ね問題ありません。ただ、言語仕様としてはvalue type(値型)と表現することが正確であることを申し添えておきます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?