BETA

Kotlinでのエラーハンドリング

投稿日:2019-12-15
最終更新:2019-12-15

まえがき

こんにちは、キョウです。英字表記だとkyou-todayを使っていますが別にkyouは今日から取ったわけではありません。

この記事はNITKC Prolab Advent Calender 2019 15日目の記事として書かれています。

前日とかに書くと確実に遅刻しそうなので13日の金曜日に書きました。

初めてのアドカレ記事なので指摘とかいただけると嬉しいです。

Kotlinでのエラーハンドリング

本題です。

Kotlinではいくつかの方法でプログラムの実行中に発生したエラーをハンドリングすることができます。

それぞれの方法の特徴と、僕なりのベストプラクティスをまとめてみたいと思います。

try-catch

皆さんご存知、Javaにもある構文ですね。

throw文で投げられた例外や実行時例外をcatchする方法です。

fun failableMethod(): Int = throw Exception()  

try {  
    failableMethod()  
} catch(ex: Exception) {  
    // caught  
    println("An error has occurred.")  
    ex.printStackTrace()  
}  

ここで注意なのですが、KotlinにはJavaでいうキャッチ例外というものが存在しません。

例を上げると、IOExceptionなどをキャッチしなくてもFiles.readAllLines(path)などができてしまいます。

Kotlinの言語思想として、例外は「復帰不可能で、発生した時点でアプリケーション全体を停止させるべきもの」に使うべきのようです。

またこの方法では、coroutinesなどで実行している非同期なコードの例外をキャッチすることができません。

runCatching-Result

1.3から導入されたResult型を使う方法です。

fun failableMethod(): Int = throw Exception()  

val result = runCatching {  
    failableMethod()  
}  

if(result.isFailture){  
        val ex = result.exceptionOrNull()  
        println("An error has occurred.")  
        ex?.printStackTrace()  
}else if(result.isSuccess){  
    val v = result.getOrNull()  
    println("Success: $v")  
}  

この方法の利点は、例外からのリカバーなどをきれいに記述することができることです。

val v = runCatching { failableMethod() }  
            .recover { 100 } // Failtureだった場合100を使用してリカバーする  
            .getOrNull() // リカバーしているのでnullが返ることはない  

また、関数型言語のような書き方をすることもできます。

runCatching { failableMethod() }  
    .onFailture { it.printStackTrace() } // it: Throwable  
    .onSuccess { println("Success: $it") } // it: Int  

runCatchingとResultの特徴として、非同期処理の例外をキャッチすることができるということがあります。

以下のコードではGlobalScope.asyncでcoroutineを使って非同期に処理をしていますが、発生する例外をきちんと拾うことができます。

val result = runCatching {  
    GlobalScope.async { failableMethod() }  
}  
if(result.isFailture) {  
    result.exceptionOrNull().printStackTrace()  
}  

また、Resultは関数の戻り値にすることができません。

runCatching内で発生する例外について、関数の呼び出し元は知る方法がないからです。

自分が呼び出した関数で発生した例外は自分が責任を持って処理するという考えがあるようです。

sealed class

Kotlinの言語機能であるsealed classを使う方法です。

sealed classはそのクラスが定義されているファイル外から継承することができません。

そのため、whenを使ったパターンマッチに向いたクラスです。

sealed class MyValue {  
    class HogeException(val message: String): MyValue()  
    object FugaException: MyValue()  
    class Success(val v): MyValue()  
}  

fun test(): MyValue = HogeException("An Error has occurred.")  

fun main() {  
    when(val value = test()) {  
        is MyValue.HogeException -> println(value.message)  
        is MyValue.FugaException -> println("FugaException")  
        is MyValue.Success -> println(value.v)  
    }  
}  

Enumなどと違い、それぞれに違う状態を与えることができます。

また、状態を保つ必要がないエラーの場合は、sealed classをobjectで継承することでシングルトンになります。

Either

これは先程のsealed classを使って、ScalaなどにあるEitherを実現するものです。

sealed class Either {  
    class Right<T>(val v: T): Either()  
    class Left<T>(val v: T): Either()  

    companion object {  
        fun <T> right(v: T): Either = Right(v)  
        fun <T> left(v: T): Either = Left(v)  
    }  
}  

やってることはsealed classと同じですね、より汎用化しただけです。

ほんとのEitherにはmapとかがあるのですが、それらも定義さえすれば使うことができます。

分解宣言

Kotlinには変数の分解宣言という機能があり、以下のようなことができます。

class Hoge {  
    fun component1() = 1  
    fun component2() = "test"  
}  

val hoge = Hoge()  
val (a, b) = hoge // a: 1,  b: "test"  
/**  
これは次のコードと同じ意味  
val hoge = Hoge()  
val a = hoge.component1()  
val b = hoge.component2()  
*/  

componentN()(Nは整数)という関数を定義することで、インスタンスから値を分解して取得することができるのです。

ここで、Kotlinのデータクラスという機能を使うと、これらcomponentN()を自動で生成してくれます。

なので次のようなコードを書くと

data class Value<S, E>(  
    val value: S? = null,  
    val error: E? = null  
)  

fun failableMethod(): Value<Int, Exception> {  
    return Value(error = Exception("An error has occurred."))  
}  

val (value, error) = failableMethod() // value: null, error: Exception("An error has occurred.")  
if(error == null) {  
    error.printStackTrace()   
    return  
}  
value!!  
println(value)  

最近流行り?のGoなどで使われる、複数値を返してエラー側がnullでないことを確認するという手法が使えます。

しかし、オートキャストは適用されないのでエラーがnullでないときvalueがnullでないと強制アサート(value!!)する必要があります。

まとめ

いっぱい挙げました。思ったより長くなってすみません。

個人的にはKotlinでのエラーは例外で表現するのではなく、sealed classなどの型として表現するほうが良いのではないかと思います。

Resultも使いやすいのですが、目的からそれないために制限が厳し目になっているので要注意です。

最後までお読みいただき、ありがとうございました。

文は長い割に内容は薄いです。

明日は宮野さん(miyacorata)がLaravelでなにかするそうです。

そちらの方も御覧ください。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@kyoutodayの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう