Featured image of post Kotlinで書いてみた〜その二〜

Kotlinで書いてみた〜その二〜

前回に続いて、今回も簡単にKotlinで色々書いてみましたのでその紹介となります。Kotlinではスタンダードライブラリや言語仕様として提供している機能がかなり多いので、これらを使いこなすだけでも生産性やコードのクォリティが大幅に上がるのではないかと思います。なので、今回もJava的な書き方を、Kotlinではどんな方法で効率よく実現できるかを中心に紹介したいと思います。

もちろんKotlinでは基本的にJavaの書き方でも全く問題なく動くコードを書けますが、Kotlinならではのコードに変えた方がより簡単で短いコードを書ける場合が多く、色々と手間を省けることができるので(そして大抵の場合、スタンダードライブラリの実装の方が自分の書いたコードよりクォリティ高いような…)こういう工夫はする価値が十分にあるのではないかと思います。

なので、今回は自分が調べたKotlinの小技を少し紹介したいと思います。

Sequentialなデータを作成する

よくユニットテストなどでテスト用データを作成して使う場合がありますね。こういう時に必要となるデータの種類は色々とあるかと思いますが、複数のレコードを番号をつけて順番に揃えた感じのものを作りたい場合もあると思います。例えばData01、Data02、Data03…といったデータを作りたい場合ですね。

この場合は、ループでデータを作り、Listにまとめるというのが一般的ではないかと思います。例えば以下のような例があるとしましょう。

// テスト用データを作成する
fun createTestDatas(): List<String> {
    // テスト用データのリスト
    val testDatas = mutableListOf<String>()
    
    // 10件のデータを追加
    for (i in 0 until 10) {
        testDatas.add("テスト$i")
    }

    // read-onlyに変換して返却
    return testDatas.toList()
}

ただ、どちらかというとこれはJavaのやり方に近いので、まずはこれをベースに、Kotlinらしきコードではどうやって同じことができるかを考えてみたいと思います。

repeat

まず考えられる方法は、ループの単純化ですね。サイズが10のリストを作りたいということは、ループが10回であることなので、それに相応しい関数を使います。例えばrepeatがありますね。repeatを使うと、スコープ内のパラメータとしてインデックスが渡されるので、簡単に

fun createTestDatas(): List<String> {
    val testDatas = mutableListOf<String>()

    // 10回繰り返す
    repeat(10) { 
        testDatas.add("テスト$i")
    }

    return testDatas.toList()
}

次に考えたいのは、MutableListImmutableに変えることです。テストで使うデータとしては問題ない場合はありますが、変更する必要のないデータをそのままMutableにしておくのはあまり良い選択ではありませんね。なので、データの作成を最初からListにできる方法を取りたいものです。

ここでは二つの道があって、最初からサイズを指定したListを宣言するか、ループの範囲、つまりRangeを指定する方法があります。

List

まずはサイズを指定したListを作る方法からみていきましょう。インスタンスの作成時に、サイズと要素に対してのイニシャライザを引数として渡すことで簡単に指定したサイズ分の要素を作ることができます。例えば、上で紹介したコードはListを使うことで以下のように変えることができます。

fun createTestDatasByList(): List<String> =
    List(10) { "テスト$it" }

この方法は、実は先に紹介した方法と根本的に違うものではありません。実装としては、以下のようになっているので、Syntax sugarとして使えるということがわかります。

@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)

@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> MutableList(size: Int, init: (index: Int) -> T): MutableList<T> {
    val list = ArrayList<T>(size)
    repeat(size) { index -> list.add(init(index)) }
    return list
}

他にもListを使う場合は、itnitとしてどんな関数を渡すかによって、stepの設定などができるのも便利ですね。例えば以下のようなことができます。

List(5) { "Test${ it * 2 }" }
// [Test0, Test2, Test4, Test6, Test8]

List(5) { (it * 2).let { index -> "$index は偶数" } }
// [0 は偶数, 2 は偶数, 4 は偶数, 6 は偶数, 8 は偶数]

ただ、結果的に作られるListのインスタンスはMutableListなので、生成したデータをread-onlyにしたい場合はまたこれをtoList()などで変換する必要があるという問題があります。

Range

では、もう一つの方法をまた試してみましょう。Kotlinでは数字の範囲を指定することだけで簡単にRangeオブジェクトを作成することができます。Rangeを使う場合、上記のコードは以下のように変えられます。

// Rangeを使ってテストデータを作る
fun createTestDatasByRange(): List<String> =
    (0..10).map { "テスト%it" }

Listの時とは違って、RangeにはIntRangeLongRangeCharRangeなどがあり、引数の数字や文字を調整することで簡単にアレンジができるということも良いです。

また、一般的に性能はListよりRangeの方が良いようです。以下のようなコードでベンチマークした際、大抵Rangeの方がListの倍ぐらい早いのを確認できました。

import kotlin.system.measureTimeMillis

data class Person(val name: String, val Num: Int)

fun main() {
    benchmark { list() }
    benchmark { range() }
}

fun benchmark(function: () -> Unit) =
    println(measureTimeMillis { function() })
    
fun list() =
    List(200000) { Person("person$it", it) }
    
fun range(): List<Person> =
    (0..200000).map { Person("person$it", it) }

一つ気にしなくてはならないのは、Rangeの場合は基本的に値が1づつ増加することになっているので、forListのようなstepの条件が使えません。なので場合によってどちらを使うかは考える必要があります。

Check

Validationなどで、パラメータの値を確認しなければならない場合があります。KotlinではNullableオブジェクトとそうでないオブジェクトが分けられているので、Javaと違って引数にnullが渡される場合はコンパイルエラーとなりますが、ビジネスロジックによってはそれ以外のことをチェックする必要もあり、自前のチェックをコードで書くしかないです。

まず、お馴染みのJavaのやり方を踏襲してみると、以下のようなコードを書くことができるでしょう。関数の引数と、その戻り値のチェックが含まれている例です。

fun doSomething(parameter: String): String {
    if (parameter.isBlank()) {
        throw IllegalArgumentException("文字列が空です")
    }

    val result = someRepository.find(parameter)

    if (result == null) {
        throw IllegalStateException("結果がnullです")
    }
    return result
}

ここで少し違う言語の例をみていきたいと思います。Kotlinとよく似ていると言われているSwiftの場合、ここでGuard Statementを使うのが一般的のようです。チェックのための表現が存在することで、ビジネスロジックとチェックが分離されるのが良いですね。Swiftをあまり触ったことがないので良い例にはなっていないかもしれませんが、イメージ的には以下のようなコードになります。

func doSomething(parameter: String) throws -> String {
    guard !parameter.isEmpty else {
        throw ValidationError.invalidArgument
    }

    guard let result = someRepository.find(parameter) else {
        throw ValidationError.notFound
    }
    return result
}

同じく、Kotlinでもチェックのための表現とビジネスロジックが分離できれば、コードの意味がより明確になるはずです。Kotlinではどうやってそれを実現できるのでしょうか。例えば以下のようなことを考えられます。

fun doSomething(parameter: String?): String {
    val checkedParameter = requireNotNull(parameter) {
        "文字列がnullです"
    }

    val result = someRepository.find(checkedParameter)

    return checkNotNull(result) {
        "結果がnullです"
    }
}

requireNotNullは、渡された引数がnullである場合はIllegalArgumentExceptionを投げ、そうでない場合は引数をnon-nullタイプとして返します、明確にnullチェックをしていることが解るだけでなく、以降チェックがいらないので便利です。また、lazy messageとしてIllegalArgumentExceptionが発生した時のメッセージを指定できるのも良いですね。

checkNotNullの場合も機能的にはrequireNotNullと変わらないですが、nullの場合に投げる例外がIllegalStateExceptionとなります。なので、用途に合わせてこの二つを分けて使えますね。

他に使えるものとしてはrequireがあります。こちらは条件式を渡すことで、nullチェック以外のこともできます。なので、以下のコードのように、Int型のデータに対して範囲をチェックするということもできるようになります。

fun doSomething(parameter: Int) {
    require(parameter > 100) {
        "$parameterは大きすぎます"
    }

    // ...
}

他にも、Elvis operatorを使う方法もありますね。この場合は、nullの場合にただ例外を投げるだけでなく、代替となる処理を書くことができますので色々と活用できる余地があります。例えば以下のようなことができますね。

fun doSomething(parameter: String?): String {
    val checkedParameter = parameter ?: "default"

    val result = someRepository.find(checkedParameter)

    return result ?: throw CustomException("結果がnullです")
}

Listの分割

とある条件と一致するデータをListから抽出したい場合は、filterのようなoperationを使うことでできます。しかし、条件が二つだとどうすればいいでしょうか。正確には、一つのリストに対して、指定した条件に一致する要素とそうでない要素の二つのリストに分離したい場合です。

こういう場合はとりあえず下記のように2回ループさせる方法があると思いますが、これはあまり効率がよくないです。

val origin = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// 奇数を抽出
val odd = origin.filter { it % 2 != 0 }
// 偶数を抽出
val even = origin.filter { it % 2 == 0 }

ループを減らすためには、あらかじめ宣言したリストに対してループの中で分岐処理を行うという方法があるでしょう。例えば以下のようにです。

val origin = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// 奇数と偶数のリストを宣言しておく
val odd = mutableListOf<Int>()
val even = mutableListOf<Int>()

// ループ処理
origin.forEach {
    if (it % 2 != 0) {
        odd.add(it) // 奇数のリストに追加
    } else {
        even.add(it) // 偶数のリストに追加
    }
}

幸い、この状況にぴったりな方法をKotlinのスタンダードライブラリが提供しています。partitionというoperationです。このopreationを使うと、元のリストの要素を条件に一致するものとそうでないもので分割してくれます。

また、partition戻り値はPair<List<T>, List<T>>なので、destructuring-declarationと組み合わせることでかなり短いコードになります。実際のコードは以下のようになるりますが、かなりスマートですね。

val origin = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val (odd, even) = origin.partition { it % 2 != 0 } // 条件に一致するものと一致しないものでリストを分離

最後に

Kotlinは便利ではありますが、言語自体が提供する便利さ(機能)が多いゆえに、APIの使い方を正しく活用できるかどうかでコードのクォリティが左右される部分が他の言語と比べ多いような気がしています。さらにバージョンアップも早く、次々と機能が追加されるのでキャッチアップも大事ですね。

でも確かに一つづつKotlinでできることを工夫するうちに、色々とできることが増えていく気もしていますね。研究すればするほど力になる言語を使うということは嬉しいことです。ということで、これからもKotlinで書いてみたシリーズは続きます。

では、また!

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy