araki tech

for developers including me

Kotlinの拡張関数とスコープ関数を使いこなそう【初学者向け】

Kotlinの拡張関数とスコープ関数を使いこなそう【初学者向け】

Kotlinの拡張関数とスコープ関数

Kotlinにはたくさんの拡張関数が存在し、その中でもよく使われるのがスコープ関数と呼ばれる拡張関数 (メソッド)があります。

この拡張関数、そしてスコープ関数を使いこなせるか否かは、Kotlinをうまく使いこなせるか否かに直結すると思っています。

もしまだ理解が乏しいと思ったならば、これを機にしっかりと定着させましょう。

拡張関数

拡張関数 (Extension functions) とは、あるクラスに対してメソッドを後から追加してそのクラスの機能性を拡張するKotlinの機能です。

一番シンプルな例として、Intのオブジェクトに対して2倍にする関数を、拡張関数として定義してみます。

/** Intの拡張関数を定義 */
fun Int.double() = this * 2

fun main() {
    println(50.double())  // 100
}

このように、拡張関数を使うと可読性が高いコーディングができます

他の例として、独自のデータクラスのリストに対して拡張関数を定義すると、より高レベルでスマートなコーディングができます。

/** 独自のデータクラス */
data class UserName(
    val firstName: String,
    val lastName: String
)

/** List<UserName>だけに適用される拡張関数 */
fun List<UserName>.hasFirstNameAs(firstName: String) = 
    this.any { it.firstName == firstName }

fun main() {
    val users = listOf(
        UserName("John", "Wick"),
        UserName("John", "Conner"),
        UserName("Hiroshi", "Araki")
    )

    println(users.hasFirstNameAs("John"))  // true
}

ちなみに、any { } も定義を見てみると以下のような拡張関数で定義されています。

public inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean {
    if (this is Collection && isEmpty()) return false
    for (element in this) if (predicate(element)) return true
    return false
}

上記の anyのようにジェネリクスを使った拡張関数はかなり汎用性が高く、一度定義しておくと便利です。

スコープ関数

そこで、Kotlinにはとても汎用性が高い拡張関数があり、スコープ関数 (Scope functions)と呼ばれています。

スコープ関数は5つあり、全て認知しておきましょう。

まずはまとめます。

スコープ関数 スコープ内で使えるオブジェクト参照 戻り値
let it ラムダの戻り値
run this ラムダの戻り値
with this ラムダの戻り値
apply this オブジェクトの参照
also it オブジェクトの参照
fun <T, R> T.let(block: (T) -> R): R
fun <T, R> T.run(block: T.() -> R): R
fun <T, R> with(receiver: T, block: T.() -> R): R
fun <T> T.apply(block: T.() -> Unit): T
fun <T> T.also(block: (T) -> Unit): T

定義を見ると、戻り値が T (呼び出し元オブジェクト)なのか R (ラムダ戻り値)なのかで用途が把握できます。

let

fun main() {
    val users = mutableListOf(
        UserName("John", "Wick")
    )

    val result = users.let {
        it.add(UserName("John", "Conner"))
        it.add(UserName("Hiroshi", "Araki"))
    }

    println(result)  // true : 最後の add の戻り値Booleanがresultに入る
}

このようにスコープ内では、itが使えます。

以下のように自身で自由に名前をつけることもできます。

val result = users.let { list ->
    list.add(UserName("John", "Conner"))
    list.add(UserName("Hiroshi", "Araki"))
}

run

fun main() {
    val users = mutableListOf(
        UserName("John", "Wick")
    )

    val result = users.run {
        add(UserName("John", "Conner"))
        add(UserName("Hiroshi", "Araki"))
    }

    println(result)  // true : 最後の add の戻り値Booleanがresultに入る
}

letと違うのはスコープ内で usersthis として扱えることです。

戻り値の扱いは let と同様です。

with

fun main() {
    val users = mutableListOf(
        UserName("John", "Wick")
    )

    val result = with(users) {
        add(UserName("John", "Conner"))
        add(UserName("Hiroshi", "Araki"))
    }

    println(result)  // true : 最後の add の戻り値Booleanがresultに入る
}

with は拡張関数ではないので少しだけ特殊です。

引数にスコープ内で this として参照したいオブジェクトを渡します。

apply

    val users = mutableListOf(
        UserName("John", "Wick")
    )

    val result = users.apply {
        add(UserName("John", "Conner"))
        add(UserName("Hiroshi", "Araki"))
    }

    println(result)  
    // [UserName(firstName=John, lastName=Wick), UserName(firstName=John, lastName=Conner), UserName(firstName=Hiroshi, lastName=Araki)]
}

スコープ内では this としてオブジェクトを扱い、戻り値はその呼び出し元のオブジェクトが返ってきます。

also

fun main() {
    val users = mutableListOf(
        UserName("John", "Wick")
    )

    val result = users.also {
        it.add(UserName("John", "Conner"))
        it.add(UserName("Hiroshi", "Araki"))
    }

    println(result)
    // [UserName(firstName=John, lastName=Wick), UserName(firstName=John, lastName=Conner), UserName(firstName=Hiroshi, lastName=Araki)]
}

applyit 版です。

それ以上の説明はありません。

スコープ関数の使い分け

最初難しいのがスコープ関数の使い分けです。

個人的によく使うのは、letapplyですがいずれのスコープ関数においても、対象のオブジェクトを一時変数で確保する必要が無くなったり、短い参照名で扱えたり、もしくは省略 (this) できたりと、そういった使い方だと思います。

例えば、Fragmentの生成などはいちいちトランザクションを一時変数で保持しなくて良くなります。

supportFragmentManager.beginTransaction().run {
    add(R.id.main_container, MyFragment())
    addToBackStack(null)
    commit()
}

この場合は、クラス内で使う自身 (Activity)の参照 this と重複してしまうので「it の方が好み」という人もいるかもしれません。

そう言う人は let を使えば良いのです。

よくある let の使い方

Kotlinはnull安全なコードで有名です。

もしNullable (null許容)な変数を扱いたいときは if 式を使わずとも let を使ったSafe-callでもっとスマートに書くことができます

fun main() {
    val users: List<UserName>? = null

    // ... 何かしらの処理でusersがnullじゃなくなったかも?

    users?.let {
        println(it.last())  // letの中では必ずNon-Nullable
    }
}

逆にやらない方が良いこと

スコープ関数のネストは辞めましょう

便利だからといって使いすぎると逆に、可読性低下に繋がります。

もしスコープ関数をどうしてもネストしたくなった場合は、スコープ内の参照に名前をつけられるletalsoを使うようにしましょう。