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
}
- 引数を { } で取る関数について疑問を持った方はまずはAndroid KotlinのonClickListnerを紐解く【初学者向け】を読んでください。
上記の 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
と違うのはスコープ内で users
が this
として扱えることです。
戻り値の扱いは 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)]
}
apply
の it
版です。
それ以上の説明はありません。
スコープ関数の使い分け
最初難しいのがスコープ関数の使い分けです。
個人的によく使うのは、let
と apply
ですがいずれのスコープ関数においても、対象のオブジェクトを一時変数で確保する必要が無くなったり、短い参照名で扱えたり、もしくは省略 (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
}
}
逆にやらない方が良いこと
スコープ関数のネストは辞めましょう。
便利だからといって使いすぎると逆に、可読性低下に繋がります。
もしスコープ関数をどうしてもネストしたくなった場合は、スコープ内の参照に名前をつけられるlet
やalso
を使うようにしましょう。