例外がスローされたらあれを実行したいが、
スローされなかったらこれを実行したい。
そういうときありますよね。
try-catch(-finally)では書きにくいときがある
try-catch(-finally)では、
例外がスローされたときの処理(とスローされてもされなくてもする処理)は書けても、
スローされなかったときの処理は単純には書けません。
例で考えてみましょう。
まず、例外がスローされることを考えなくてよい場合を考えます。
fun noException() {
val myValue =
getMyValue() // 値を取得する。
.also {
onMySuccess(it) // その後 onMySuccess を呼び出す。
onMyFinally() // 最後に onMyFinally を呼び出す。
}
setMyValue(myValue)
}
ここで getMyValue() が例外をスローする可能性があるとしましょう。
例外がスローされたときには onMyFailure 関数を呼び出して代わりの値を取得するようしましょう。
簡単に考えるとこうなります。
fun useTryCatch1() {
val myValue =
try {
getMyValue() // getMyValue() は例外をスローする可能性がある。
.also { onMySuccess(it) } // 例外がスローされなかったら onMySuccess を呼び出す。
} catch (e: MyException) {
onMyFailure(e) // 例外がスローされたら onMyFailure を呼び出し、代わりの値を取得する。
} finally {
onMyFinally() // 最後に onMyFinally を呼び出す。
}
setMyValue(myValue)
}
ですがこれはあまりよくありません。try の中に関係ない onMySuccess 関数呼び出しも入ってしまっています。
もし不具合により誤って onMySuccess が例外をスローしてしまったときに、それが握りつぶされてしまい、その不具合の発見が遅れてしまいます。
onMySuccess を try ブロックの外に出しましょう。
fun useTryCatch2() {
val myValue =
run {
try {
getMyValue()
} catch (e: Throwable) {
return@run onMyFailure(e)
}.also {
onMySuccess(it)
}
}.also {
onMyFinally()
}
setMyValue(myValue)
}
…うーん、複雑でわかりにくくなってしまいました。
runCatching 関数を使ってみよう
では try-catch の代わりに runCatching 関数を使ってみましょう。
fun useRunCatching() {
val myValue =
runCatching {
getMyValue()
}.fold(
onSuccess = { it.also { onMySuccess(it) } },
onFailure = { onMyFailure(it) }
).also {
onMyFinally()
}
setMyValue(myValue)
}
シンプル!
try-catch-finally に、例外がスローされなかったときの処理ブロックが追加されたような、わかりやすい構造ですね!
Result クラス — runCatching 関数の返値型
runCatching 関数は、与えられたブロックを実行し、 Result 型のオブジェクトを返します。
val result: Result<MyValue> = runCatching { MyValue() }
もしブロック内で例外がスローされても、runCatching 関数はその例外を外にスローしません。
ブロックを実行した結果の返値や例外は、runCatching 関数が返した Result オブジェクトから取得することができます。
Result クラスのメンバ
プロパティ isSuccess と isFailure
ブロックの実行に成功した(ブロック内で例外がスローされなかった)かどうかは isSuccess プロパティから知ることができます。
逆にブロックの実行に失敗した(例外がスローされた)かどうかは isFailure プロパティから知ることができます。
if (result.isSuccess) { println("成功") }
if (result.isFailure) { println("失敗") }
関数 getOrNull と exceptionOrNull
ブロックを実行した結果の返値を得るには getOrNull 関数を使います。
ブロック内で例外がスローされた場合は null が返ります。
ブロック内でスローされた例外を取得するには exceptionOrNull 関数を使います。
スローされなかった場合は null が返ります。
result.getOrNull()
?.also { println("返値は $it です。") }
?: println("例外がスローされました。")
result.exceptionOrNull()
?.also { println("例外は $it です。") }
?: println("値が返されました。")
Result クラスの拡張関数
Result クラスのメンバは先ほど述べた4つ(とtoString)だけです。
ですが拡張関数が多数用意されており、これらがとても便利です。
get 系関数
get 系関数は、ブロックの実行に成功したときにはその結果の返値を返します。
失敗したときの動作はそれぞれ異なります。
getOrThrow 関数:
ブロック内でスローされた例外をそのままスローします。
getOrDefault 関数:
指定されたデフォルト値を返します。
getOrElse 関数:
指定された処理を実行します。
val result = runCatching { "".toInt() }
try {
result.getOrThrow()
} catch (e: Exception) {
println("0") // > 0
}
result.getOrDefault(1).let { println(it) } // > 1
result.getOrElse { println(2) } // > 2
on 系関数
onSuccess 関数のブロックは、runCatching 関数のブロックの実行が成功したときだけ実行されます。
onFailure 関数のブロックは、runCatching 関数のブロックの実行が失敗したときだけ実行されます。
これらはレシーバーの Result オブジェクトをそのまま返すので、チェイン呼び出しできます。
result
.onSuccess { onMySuccess(it) }
.onFailure { onMyFailure(it) }
これらはインライン関数なので、ブロック内から return したり throw したりできます。
fun myFun() {
run {
result
.onSuccess { return@run } // 成功したら run ブロックから抜ける
.onFailure { throw Exception() } // 失敗したら myFun 関数から例外をスローする
}
}
map 系関数と recover 系関数
ここでは次のように例外クラスと関数が定義されているとします。
class MyIntException : Exception()
class MyDoubleException : Exception()
fun throwMyIntException(): Int = throw MyIntException()
fun throwMyDoubleException(): Double = throw MyDoubleException()
map 系関数
map 関数と mapCatching 関数は runCatching 関数のブロックを実行した結果の返値を任意の型の別の値に変換します。
runCatching { 3.14 }
.map { it.toInt() }
.onSuccess { println(it) } // > 3
runCatching { 3.14 }
.mapCatching { it.toInt() }
.onSuccess { println(it) } // > 3
ただし、変換処理中に例外がスローされたときの動作が異なります。
map 関数ではその例外を外にスローします。
mapCatching では同関数が返す Result が失敗になり、その失敗原因がその例外になります。
try {
runCatching { 3.14 }
.map { throwMyIntException() }
.onSuccess { println(it) } // このラムダ式は実行されない。
.onFailure { println(it) } // このラムダ式は実行されない。
} catch (e: MyIntException) {
println(e) // > MyIntException
}
runCatching { 3.14 }
.mapCatching { throwMyIntException() }
.onFailure { println(it) } // > MyIntException
recover 系関数
関数 recover と recoverCatching は runCatching 関数のブロックの実行が失敗したときにそれを成功に変えます。
runCatching { throwMyDoubleException() }
.recover { 3 }
.onSuccess { println(it) } // > 3
runCatching { throwMyDoubleException() }
.recoverCatching { 3 }
.onSuccess { println(it) } // > 3
ただし、変換中に例外がスローされたときの動作が異なります。
recover 関数はその例外を外にスローします。
recoverCatching 関数では同関数が返す Result が失敗になり、その失敗原因がその例外になります。
try {
runCatching { throwMyDoubleException() }
.recover { throwMyIntException() }
.onSuccess { println(it) } // このラムダ式は実行されない。
.onFailure { println(it) } // このラムダ式は実行されない。
} catch (e: MyIntException) {
println(e) // > MyIntException
}
runCatching { throwMyDoubleException() }
.recoverCatching { throwMyIntException() }
.onFailure { println(it) } // > MyIntException
fold 関数
fold 関数は runCatching 関数のブロックの実行が成功したときには引数 onSuccess で与えられた処理を行い、失敗したときには引数 onFailure で与えられた処理を行います。
先ほど紹介した関数 onSuccess と onFailure を両方呼び出すのと似ていますが、1つ大きな違いがあります。
それは返値です。
fold 関数では、成功したときの処理と失敗したときの処理がともに同じ型の返値を持つ必要があり、それが fold 関数の返値になります。
val double = runCatching { "".toInt() }.fold(
onSuccess = { it.toDouble() },
onFailure = { Double.NaN },
)
try-catch 式で返値を使う場合の代替にはこれを使うことになるでしょう。
注意
【〜Kotlin 1.4】Result は関数の返値には使えない
Kotlin 1.4 まででは、Result は関数の返値には使えません。
fun getResult(): Result<Int> { // コンパイルエラー!
return runCatching { 1 }
}
Kotlin 1.5 からは使えるようになりました。
まとめ
try-catch の代替となる runCatching 関数と、その返値となる Result クラスについて説明しました。