- Kotlin
Kotlin when式の使い方|switchとの違い・型判定・sealed
Kotlin の when 式とは、1つの値や複数の条件を上から順に調べ、最初に当てはまった分岐の結果を返す構文です。 Java の switch に相当しますが、break が不要で、範囲(in)や型(is)でも分岐でき、式として値をそのまま変数に入れられます。
この記事では、when の書き方を7種類の一覧表にまとめ、すべてのコードに実行結果を付けました。さらに、Java の switch との対応表、sealed interface と enum で分岐を1つ消したときに出る 'when' expression must be exhaustive のエラー文、そして「else を書かないほうがよい」理由を、型を1つ追加する流れで確かめます。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。掲載した出力とエラー文は、すべてこの環境でコンパイル・実行して得たものです。
when式の書き方の早見表
| やりたいこと | 書き方 | 例 | 当てはまる条件 |
|---|---|---|---|
| 値で分岐 | 値 -> |
1 -> "one" |
subject がその値と等しい |
| 複数の値をまとめる | 値, 値 -> |
"Sat", "Sun" -> "休日" |
どれか1つと等しい |
| 範囲で分岐 | in 範囲 -> |
in 90..100 -> "A" |
範囲やコレクションに含まれる |
| 範囲の外 | !in 範囲 -> |
!in 0..100 -> "範囲外" |
含まれない |
| 型で分岐 | is 型 -> |
is String -> v.length |
その型である(右側で型が確定する) |
| 条件式を並べる | when { 条件 -> } |
stock == 0 -> "在庫なし" |
条件が true |
| 値を変数に受ける | when (val x = 式) |
when (val code = fetchStatus(id)) |
分岐の中で x を使える |
| 条件を追加する | 条件 if 追加条件 -> |
is Int if v < 0 -> "負の数" |
両方が true(Kotlin 2.2 以降) |
| どれにも当てはまらない | else -> |
else -> "other" |
上のどれも当てはまらない |
subject(サブジェクト) とは、when (x) のかっこの中に書く、調べる対象の値のことです。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin when式の使い方|switchとの違い・型判定・sealed」
まず動かす:7つの書き方を1つのファイルで確認する
次のコードを forms.kt という名前で保存すると、そのままコンパイルして実行できます。7つの関数が、早見表の書き方にそれぞれ対応しています。
// forms.kt : when の書き方 7 種類
fun byValue(x: Int): String = when (x) {
1 -> "one"
2 -> "two"
else -> "other"
}
fun byMultiple(day: String): String = when (day) {
"Sat", "Sun" -> "休日"
else -> "平日"
}
fun byRange(score: Int): String = when (score) {
in 90..100 -> "A"
in 70..<90 -> "B"
!in 0..100 -> "範囲外"
else -> "C"
}
fun byType(v: Any): String = when (v) {
is String -> "String 長さ=${v.length}"
is Int -> "Int 2倍=${v * 2}"
is List<*> -> "List 件数=${v.size}"
else -> "その他"
}
fun noSubject(stock: Int, reserved: Int): String = when {
stock == 0 -> "在庫なし"
stock - reserved < 5 -> "残りわずか"
else -> "在庫あり"
}
fun fetchStatus(id: Int): Int = if (id == 1) 200 else 404
fun subjectVar(id: Int): String = when (val code = fetchStatus(id)) {
200 -> "OK"
else -> "エラー code=$code"
}
fun guard(v: Any): String = when (v) {
is String if v.isEmpty() -> "空の文字列"
is String -> "文字列: $v"
is Int if v < 0 -> "負の数"
is Int -> "0以上の数"
else -> "その他"
}
fun main() {
println("値 : ${byValue(1)} / ${byValue(9)}")
println("複数値 : ${byMultiple("Sun")} / ${byMultiple("Mon")}")
println("範囲 : ${byRange(95)} / ${byRange(70)} / ${byRange(50)} / ${byRange(120)}")
println("型 : ${byType("abc")} / ${byType(21)} / ${byType(listOf(1, 2))} / ${byType(1.5)}")
println("引数なし: ${noSubject(0, 0)} / ${noSubject(10, 7)} / ${noSubject(50, 3)}")
println("変数 : ${subjectVar(1)} / ${subjectVar(2)}")
println("ガード : ${guard("")} / ${guard("abc")} / ${guard(-3)} / ${guard(8)}")
}
コンパイルと実行のコマンドは次のとおりです。-Dstdout.encoding=UTF-8 は、日本語の出力を UTF-8 で表示するための指定です。
kotlinc forms.kt -include-runtime -d forms.jar
java -Dstdout.encoding=UTF-8 -jar forms.jar
実行結果:
値 : one / other
複数値 : 休日 / 平日
範囲 : A / B / C / 範囲外
型 : String 長さ=3 / Int 2倍=42 / List 件数=2 / その他
引数なし: 在庫なし / 残りわずか / 在庫あり
変数 : OK / エラー code=404
ガード : 空の文字列 / 文字列: abc / 負の数 / 0以上の数
7つの書き方は、同じ when の中で自由に混ぜられます。 以降のセクションで、1つずつ要点を見ていきます。
値・複数の値で分岐する
いちばん基本の形です。-> の左に値、右に結果を書きます。
fun byValue(x: Int): String = when (x) {
1 -> "one"
2 -> "two"
else -> "other"
}
複数の値で同じ結果にしたいときは、カンマで並べます。Java の switch で case を縦に並べる書き方に当たります。
fun byMultiple(day: String): String = when (day) {
"Sat", "Sun" -> "休日"
else -> "平日"
}
-> の左側には、リテラル(数値や文字列をそのまま書いたもの)だけでなく、定数・関数の戻り値・in によるコレクションの判定も書けます。次のコードで確かめます。
// extra.kt(抜粋して単体で動く形)
const val MAX = 100
fun limit(): Int = 50
fun check(x: Int): String = when (x) {
MAX -> "定数 MAX と同じ"
limit() -> "関数 limit() の戻り値と同じ"
in listOf(1, 3, 5) -> "リストに含まれる"
in 10..20 step 5 -> "10・15・20 のどれか"
else -> "どれでもない"
}
fun main() {
println(listOf(100, 50, 3, 15, 12).map { check(it) })
}
実行結果:
[定数 MAX と同じ, 関数 limit() の戻り値と同じ, リストに含まれる, 10・15・20 のどれか, どれでもない]
Java の従来の switch では case に定数しか書けませんが、Kotlin の when は任意の式を書けます。 この自由さが、when が幅広く使われる理由の1つです。
分岐の中に複数の文を書く
-> の右側を波かっこで囲むと、複数の文を書けます。ブロックの最後の式が、その分岐の値になります。
fun block(x: Int): String = when {
x % 2 == 0 -> {
val half = x / 2
"偶数(半分は $half)"
}
else -> "奇数"
}
fun main() {
println("${block(8)} / ${block(7)}")
}
実行結果:
偶数(半分は 4) / 奇数
範囲(in)で分岐する
in を使うと、値が範囲に入っているかで分岐できます。点数や年齢、件数のように「〜以上〜未満」で分けたいときに向いています。
fun byRange(score: Int): String = when (score) {
in 90..100 -> "A"
in 70..<90 -> "B"
!in 0..100 -> "範囲外"
else -> "C"
}
| 書き方 | 意味 | 例の範囲 |
|---|---|---|
in 90..100 |
90 以上 100 以下 | 両端を含む |
in 70..<90 |
70 以上 90 未満 | 終わりを含まない |
!in 0..100 |
0〜100 に入らない | 範囲の外 |
in listOf(1, 3, 5) |
リストに含まれる | コレクションにも使える |
in 10..20 step 5 |
10・15・20 のどれか | 刻み幅つき |
上から順に調べ、最初に当てはまった分岐だけが動く
範囲が重なっている場合は、上に書いた分岐が優先されます。次の例では 7 が両方の範囲に入りますが、先に書いた「1〜10」だけが選ばれます。
fun order(x: Int): String = when (x) {
in 1..10 -> "1〜10"
in 5..20 -> "5〜20"
else -> "その他"
}
fun main() {
println("${order(7)} / ${order(15)}")
}
実行結果:
1〜10 / 5〜20
条件が狭いもの・特別なものを上に、広いものを下に書くと、意図どおりに動きます。
型(is)で分岐する:スマートキャストが効く
is を使うと、値の型で分岐できます。-> の右側では、その型として扱えるので、キャスト(型の変換)を自分で書く必要がありません。この仕組みをスマートキャストと呼びます。
fun byType(v: Any): String = when (v) {
is String -> "String 長さ=${v.length}" // v は String として使える
is Int -> "Int 2倍=${v * 2}" // v は Int として使える
is List<*> -> "List 件数=${v.size}"
else -> "その他"
}
実行結果(byType("abc")、byType(21)、byType(listOf(1, 2))、byType(1.5) の順):
型 : String 長さ=3 / Int 2倍=42 / List 件数=2 / その他
v の型は Any ですが、is String の分岐の中では v.length を呼べています。!is String と書けば「その型ではない」という条件になります。
null を分岐に書く
null 許容型(String? のように null が入るかもしれない型)では、null をそのまま分岐の値に書けます。null の分岐を通ったあとの分岐では、値は null でないものとして扱われます。
fun nullable(v: String?): String = when (v) {
null -> "null"
"" -> "空"
else -> "長さ ${v.length}" // ここでは v は String
}
fun main() {
println("${nullable(null)} / ${nullable("")} / ${nullable("abc")}")
}
実行結果:
null / 空 / 長さ 3
引数なしの when:if - else if の置き換え
かっこと subject を省略すると、各分岐に Boolean の条件式を書けます。複数の変数が関わる条件や、大小比較を並べたいときに使います。
fun noSubject(stock: Int, reserved: Int): String = when {
stock == 0 -> "在庫なし"
stock - reserved < 5 -> "残りわずか"
else -> "在庫あり"
}
if - else if が続くコードは、引数なしの when に置き換えると見通しがよくなります。直す前と後を並べます。
// 直す前: if - else if の連なり
fun gradeIf(score: Int): String {
if (score >= 90) {
return "A"
} else if (score >= 70) {
return "B"
} else if (score >= 50) {
return "C"
} else {
return "D"
}
}
// 直した後: when 式
fun gradeWhen(score: Int): String = when {
score >= 90 -> "A"
score >= 70 -> "B"
score >= 50 -> "C"
else -> "D"
}
fun main() {
val scores = listOf(95, 75, 55, 10)
println(scores.map { gradeIf(it) } == scores.map { gradeWhen(it) })
println(scores.map { gradeWhen(it) })
}
実行結果:
true
[A, B, C, D]
結果は同じで、行数は11行から6行になりました。条件と結果が1行ずつ対応するので、表を読むように確認できます。
when は式なので、文字列テンプレートの中や関数の引数にも直接書けます。
fun main() {
val n = 7
println("$n は ${when { n % 2 == 0 -> "偶数"; else -> "奇数" }}")
}
実行結果:
7 は 奇数
subject を変数に受ける:when (val x = ...)
関数の戻り値で分岐し、その値を分岐の中でも使いたいときは、かっこの中で変数を宣言できます。
fun fetchStatus(id: Int): Int = if (id == 1) 200 else 404
fun subjectVar(id: Int): String = when (val code = fetchStatus(id)) {
200 -> "OK"
else -> "エラー code=$code"
}
実行結果(subjectVar(1) と subjectVar(2)):
変数 : OK / エラー code=404
公式ドキュメントには、この変数のスコープ(使える範囲)は when の本体に限られると書かれています。when の外に変数を残さないので、同じ名前を別の場所で使っても混ざりません。
null かどうかの確認と組み合わせる使い方も便利です。throw を分岐に書くこともできます。
fun parse(s: String): Int = when (val n = s.toIntOrNull()) {
null -> throw IllegalArgumentException("数値ではありません: $s")
else -> n // ここでは n は Int
}
fun main() {
println(parse("42"))
try {
parse("abc")
} catch (e: IllegalArgumentException) {
println("例外: ${e.message}")
}
}
実行結果:
42
例外: 数値ではありません: abc
ガード条件:分岐に if で条件を足す(Kotlin 2.2 以降)
ガード条件とは、分岐の条件のうしろに if 追加条件 を書いて、条件を絞り込む書き方です。Kotlin 公式の「What's new in Kotlin 2.2.0」には、2.1.0 でプレビューとして入り、2.2.0 で Stable(安定版)になったと書かれています。
fun guard(v: Any): String = when (v) {
is String if v.isEmpty() -> "空の文字列"
is String -> "文字列: $v"
is Int if v < 0 -> "負の数"
is Int -> "0以上の数"
else -> "その他"
}
実行結果(guard("")、guard("abc")、guard(-3)、guard(8) の順):
ガード : 空の文字列 / 文字列: abc / 負の数 / 0以上の数
is String if v.isEmpty() は「String であり、かつ空である」という意味です。is String の判定が先に行われるので、ガード条件の中でも v を String として使えます。
else if も書ける
else にもガード条件を付けられます。
fun guardElseIf(v: Any, strict: Boolean): String = when (v) {
is Int if v > 100 -> "大きい数"
is Int -> "数"
else if strict -> "数以外(厳格)"
else -> "数以外"
}
fun main() {
println("${guardElseIf(500, false)} / ${guardElseIf(5, false)} / ${guardElseIf("a", true)} / ${guardElseIf("a", false)}")
}
実行結果:
大きい数 / 数 / 数以外(厳格) / 数以外
ガード条件を書ける場所の決まり
ガード条件には2つの決まりがあります。実際にコンパイルして、エラー文を確かめました。
fun c(v: Any): String = when (v) {
is String, is Int if v.toString().isEmpty() -> "empty" // カンマ区切りと併用
else -> "other"
}
fun d(x: Int): String = when {
x > 0 if x < 10 -> "1桁" // 引数なしの when で使用
else -> "other"
}
コンパイル結果:
misc_ng.kt:15:23: error: use of comma in 'when' condition with guard statement is not allowed.
misc_ng.kt:20:11: error: guard statements are only allowed in 'when' with subject.
| 決まり | エラー文 | 書き換え方 |
|---|---|---|
| カンマ区切りの分岐には付けられない | use of comma in 'when' condition with guard statement is not allowed. | 分岐を2行に分ける |
| subject のある when だけで使える | guard statements are only allowed in 'when' with subject. | 引数なしの when では x > 0 && x < 10 と書く |
Java の switch との違い:対応表
Java から Kotlin に移るときに、最初に知っておきたい違いを表にしました。
| 比べる点 | Java の従来の switch 文 | Java 14 以降の switch 式 | Kotlin の when |
|---|---|---|---|
| キーワード | switch / case / default |
switch / case -> / default |
when / -> / else |
| break | 必要(書かないと次の case へ進む) | 不要 | 不要(次の分岐へ進む仕組みが無い) |
| 値を返す | 返さない(文) | 返す | 返す(文としても使える) |
| 複数の値 | case を縦に並べる | case 1, 2 -> |
1, 2 -> |
| 範囲 | 書けない | 書けない | in 1..10 -> |
| 型で分岐 | 書けない | Java 21 から case String s -> |
is String -> |
| 条件の追加 | 書けない | Java 21 から case X x when 条件 -> |
is X if 条件 ->(2.2 以降) |
| 条件式だけを並べる | 書けない | 書けない | when { 条件 -> } |
| その他の分岐 | default(省略可) |
default(網羅できていれば省略可) |
else(網羅できていれば省略可) |
Java の when は switch のガードに使うキーワードで、Kotlin の when は分岐の構文そのものです。同じ単語でも役割が違う点に気をつけると、読み替えがスムーズです。
同じ処理を Java と Kotlin で動かす
Java 21 で、従来の switch 文と switch 式を並べて実行しました。
// Sw.java
public class Sw {
public static void main(String[] args) {
int x = 1;
// 従来の switch 文: break を書かないと次の case へ進む
switch (x) {
case 1:
System.out.println("one");
case 2:
System.out.println("two");
break;
default:
System.out.println("other");
}
// Java 14 以降の switch 式: -> で書くと break 不要で値を返す
String s = switch (x) {
case 1 -> "one";
case 2 -> "two";
default -> "other";
};
System.out.println(s);
}
}
実行結果(java Sw.java):
one
two
one
case 1: に break が無いため、1行目と2行目で "one" と "two" の両方が出力されています。この「次の case へ進む」動きをフォールスルーと呼びます。3行目は switch 式の結果です。
同じ処理を Kotlin で書きます。
// sw.kt
fun main() {
val x = 1
val s = when (x) { // 式として使う: else が必要
1 -> "one"
2 -> "two"
else -> "other"
}
println(s)
when (x) { // 文として使う: else を省略できる
1 -> println("one")
2 -> println("two")
}
}
実行結果:
one
one
Kotlin の when にはフォールスルーが無く、当てはまった分岐だけが動きます。 複数の値で同じ処理をしたいときは、カンマで並べます。
Java 21 のパターンマッチングとの対応
Java 21 の switch は、sealed interface の型で分岐し、when でガードを付けられます。Kotlin の is とガード条件に対応する書き方です。
// Pm.java
public class Pm {
sealed interface Result permits Success, Failure, Loading {}
record Success(String body) implements Result {}
record Failure(int code) implements Result {}
record Loading() implements Result {}
static String describe(Result r) {
return switch (r) {
case Success s -> "成功: " + s.body();
case Failure f when f.code() >= 500 -> "サーバー側のエラー: " + f.code();
case Failure f -> "エラー: " + f.code();
case Loading l -> "読み込み中";
};
}
public static void main(String[] args) {
System.out.println(describe(new Success("hello")));
System.out.println(describe(new Failure(503)));
System.out.println(describe(new Failure(404)));
System.out.println(describe(new Loading()));
}
}
実行結果:
成功: hello
サーバー側のエラー: 503
エラー: 404
読み込み中
case Loading l の行を消してコンパイルすると、Java でも網羅チェックのエラーが出ます。
Pm.java:8: error: the switch expression does not cover all possible input values
Java と Kotlin のどちらも「すべての型を扱っているか」をコンパイラが調べてくれます。次のセクションで、Kotlin 側の動きを詳しく見ます。
else はいつ必要か:式と文の違い
when を式として使う(値を受け取る)ときは、すべての場合を網羅する必要があります。網羅できていないと、コンパイルエラーになります。
fun a(x: Int): String = when (x) {
1 -> "one"
2 -> "two"
}
コンパイル結果:
misc_ng.kt:3:25: error: 'when' expression must be exhaustive. Add an 'else' branch.
fun a(x: Int): String = when (x) {
^^^^
Int はすべての値を列挙できないので、else を足すよう案内されます。
一方、文として使う(値を受け取らない)ときは、Int や String が subject なら else を省略できます。当てはまる分岐が無ければ、何も実行せずに次へ進みます。
fun stmt(x: Int) {
when (x) {
1 -> println("one")
2 -> println("two")
}
println("stmt($x) 終了")
}
fun main() {
stmt(1)
stmt(3)
}
実行結果:
one
stmt(1) 終了
stmt(3) 終了
else の要否の一覧
| subject の型 | 式として使う | 文として使う |
|---|---|---|
| Int・String など | else が必要 | else を省略できる |
| Boolean | true と false を書けば else 不要 | true と false の両方か else が必要 |
| enum class | 全部の定数を書けば else 不要 | 全部の定数か else が必要 |
| sealed class / sealed interface | 全部の型を書けば else 不要 | 全部の型か else が必要 |
| 上の3つの null 許容型 | null -> も書けば else 不要 |
null -> も含めた全部か else が必要 |
enum を subject にした when は、文として使う場合も網羅が求められます。次のコードで確かめました。
enum class Size { S, M, L }
fun b(size: Size) {
when (size) {
Size.S -> println("small")
}
}
コンパイル結果:
misc_ng.kt:9:5: error: 'when' expression must be exhaustive. Add the 'M', 'L' branches or an 'else' branch.
when (size) {
^^^^
Boolean と null 許容の enum は、else なしで次のように書けます。
enum class Size { S, M, L }
fun label(size: Size?): String = when (size) {
Size.S -> "小"
Size.M -> "中"
Size.L -> "大"
null -> "未選択"
}
fun flag(on: Boolean): String = when (on) {
true -> "ON"
false -> "OFF"
}
fun main() {
println("${label(Size.M)} / ${label(null)}")
println("${flag(true)} / ${flag(false)}")
}
実行結果:
中 / 未選択
ON / OFF
sealed interface と enum:分岐を1つ消すとどうなるか
sealed interface(シールドインターフェース)とは、実装する型の種類をあらかじめ決めておけるインターフェースです。コンパイラが「この型は全部で何種類か」を知っているため、when で全種類を扱っているかを調べられます。
まず、すべての分岐がそろった動くコードです。
// sealed_ok.kt
sealed interface Result
data class Success(val body: String) : Result
data class Failure(val code: Int) : Result
data object Loading : Result
enum class Size { S, M, L }
fun describe(r: Result): String = when (r) {
is Success -> "成功: ${r.body}"
is Failure -> "エラー: ${r.code}"
Loading -> "読み込み中"
}
fun price(size: Size): Int = when (size) {
Size.S -> 300
Size.M -> 400
Size.L -> 500
}
fun main() {
println(describe(Success("hello")))
println(describe(Failure(404)))
println(describe(Loading))
println(Size.entries.map { "$it=${price(it)}" })
}
実行結果:
成功: hello
エラー: 404
読み込み中
[S=300, M=400, L=500]
data object(データオブジェクト)は、インスタンスが1つだけの型です。型ではなく値として比べられるので、is を付けずに Loading -> と書けます。
分岐を1つずつ消してコンパイルする
Loading -> "読み込み中" の行と Size.L -> 500 の行を消して、コンパイルしました。
sealed_ng.kt:8:35: error: 'when' expression must be exhaustive. Add the 'Loading' branch or an 'else' branch.
fun describe(r: Result): String = when (r) {
^^^^
sealed_ng.kt:13:30: error: 'when' expression must be exhaustive. Add the 'L' branch or an 'else' branch.
fun price(size: Size): Int = when (size) {
^^^^
エラー文の中に、足りない分岐の名前('Loading'、'L')が出ています。 どこに何を足せばよいかが、エラー文だけでわかります。
| エラー文 | 状況 | 直し方 |
|---|---|---|
| Add an 'else' branch. | Int や String など、列挙できない型 | else -> を足す |
| Add the 'Loading' branch or an 'else' branch. | sealed の型が1つ足りない | Loading -> を足す |
| Add the 'L' branch or an 'else' branch. | enum の定数が1つ足りない | Size.L -> を足す |
| Add the 'M', 'L' branches or an 'else' branch. | 複数足りない | 足りない分岐をすべて足す |
sealed と enum では else を書かないほうがよい理由
エラー文は「分岐を足すか、else を足すか」の2つを案内しています。sealed と enum の場合は、分岐を足すほうを選ぶのがおすすめです。理由を、型を1つ追加する流れで確かめます。
else を書いた場合:型を足しても気づけない
Result に Timeout という型をあとから追加したとします。when 側は else で受けています。
// sealed_else.kt
sealed interface Result
data class Success(val body: String) : Result
data class Failure(val code: Int) : Result
data object Loading : Result
data object Timeout : Result // あとから追加した型
fun withElse(r: Result): String = when (r) {
is Success -> "成功: ${r.body}"
is Failure -> "エラー: ${r.code}"
else -> "読み込み中"
}
fun main() {
println(withElse(Timeout))
}
実行結果:
読み込み中
コンパイルは通り、Timeout なのに「読み込み中」と表示されました。else が新しい型も受け止めるため、対応が必要な場所をコンパイラが知らせてくれません。
else を書かない場合:コンパイラが場所を教えてくれる
同じ追加を、else なしの when に対して行います。
// sealed_noelse.kt
sealed interface Result
data class Success(val body: String) : Result
data class Failure(val code: Int) : Result
data object Loading : Result
data object Timeout : Result // あとから追加した型
fun withoutElse(r: Result): String = when (r) {
is Success -> "成功: ${r.body}"
is Failure -> "エラー: ${r.code}"
Loading -> "読み込み中"
}
fun main() {
println(withoutElse(Timeout))
}
コンパイル結果:
sealed_noelse.kt:7:38: error: 'when' expression must be exhaustive. Add the 'Timeout' branch or an 'else' branch.
fun withoutElse(r: Result): String = when (r) {
^^^^
型を足した瞬間に、対応が必要な when がすべてコンパイルエラーとして一覧になります。 実行する前に直せるので、修正もれを防げます。
| 書き方 | 型を追加したとき | 向いている場面 |
|---|---|---|
| else を書く | コンパイルが通り、else の結果になる | Int・String など列挙できない型、または「その他はすべて同じ扱い」と決まっているとき |
| else を書かない | コンパイルエラーで場所がわかる | sealed・enum・Boolean で、種類ごとに扱いを決めたいとき |
when を読みやすく書くチェックリスト
| 確認すること | 理由 |
|---|---|
| 特別な条件・狭い範囲を上に書いたか | 上から順に調べ、最初に当てはまった分岐だけが動くため |
| sealed・enum で else を使っていないか | 型を足したときにコンパイラが場所を教えてくれるため |
| 値を受け取るなら式として書いたか | val s = when ... と書くと、代入もれが起きない |
関数の戻り値で分岐するなら when (val x = ...) を使ったか |
変数が when の外に残らない |
| 複数の変数が関わるなら引数なしの when にしたか | 条件式をそのまま並べられる |
| 同じ型で条件が分かれるならガード条件を使ったか | when の入れ子を減らせる |
| 分岐が長くなったら関数に切り出したか | -> の右が1行だと表のように読める |
2つの値の組み合わせで分岐する
2つの値を同時に見たいときは、引数なしの when に条件を並べるのが素直です。x to y で Pair(2つの値の組)を作って比べる書き方もできます。
fun pair(x: Int, y: Int): String = when (x to y) {
0 to 0 -> "原点"
else -> when {
x == 0 -> "Y軸上"
y == 0 -> "X軸上"
else -> "それ以外"
}
}
fun main() {
println("${pair(0, 0)} / ${pair(0, 3)} / ${pair(2, 0)} / ${pair(2, 3)}")
}
実行結果:
原点 / Y軸上 / X軸上 / それ以外
when の中に when を書くこともできますが、入れ子が深くなったら関数に分けると読みやすさを保てます。
よくある質問
Q. when と if はどう使い分けますか? A. 分岐が2つなら if、3つ以上なら when が読みやすくなります。どちらも式なので値を返せます。同じ値をいくつもの候補と比べるとき、範囲や型で分けるときは when が向いています。
Q. when で break や continue は必要ですか? A. 分岐を抜けるための break は不要です。当てはまった分岐だけが動き、次の分岐へは進みません。Java の従来の switch 文で break を書かなかった場合の「次の case へ進む」動きは、Kotlin の when にはありません。
Q. 「'when' expression must be exhaustive」と出たらどうすればよいですか? A. エラー文の「Add the '〇〇' branch」に書かれた分岐を足します。subject が sealed や enum のときは足りない名前が表示されるので、その分岐を書くのがおすすめです。Int や String のときは「Add an 'else' branch.」と出るので else を足します。
Q. ガード条件はどのバージョンから使えますか? A. Kotlin 公式の「What's new in Kotlin 2.2.0」に、2.1.0 でプレビュー、2.2.0 で Stable になったと書かれています。この記事では Kotlin 2.4.20 で、追加の設定なしにコンパイルできることを確かめました。
まとめ
- when は「値・複数値・範囲(in)・型(is)・引数なし・subject変数・ガード条件」の7つの書き方を混ぜて使える
- break は不要で、上から順に調べて最初に当てはまった分岐だけが動く
- 式として使うときは網羅が必要。Int や String では else を書く
- is で分岐すると右側でその型として扱える(スマートキャスト)
- ガード条件(条件 if 追加条件)は Kotlin 2.2 以降。subject のある when で、カンマ区切りでない分岐に書ける
- sealed・enum では else を書かず、全部の分岐を並べると、型を足したときにコンパイラが場所を教えてくれる
- エラー文 'when' expression must be exhaustive には足りない分岐の名前が出るので、それを足せば解決する
参考・出典
- Kotlin 公式ドキュメント「Conditions and loops」(when 式、ガード条件、網羅の決まり): https://kotlinlang.org/docs/control-flow.html
- Kotlin 公式ドキュメント「What's new in Kotlin 2.2.0」(ガード条件の Stable 化): https://kotlinlang.org/docs/whatsnew22.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin when式の使い方|switchとの違い・型判定・sealed」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。