例外がスローされたらあれを実行したいが、
スローされなかったらこれを実行したい。
そういうときありますよね。
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
クラスについて説明しました。