Android
Kotlin
android開発

Kotlinのapplyをじっくりと読む

Kotlinのapplyを理解していく過程で色々勉強になったので、その過程をそのまま✏️。

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

kotlin.internal.contracts.Standard.ktより)



ひとつずつ何が書いてあるか理解していく。

publicの部分

public は可視性修飾子。どこからでも参照可能であることを意味している。

public


inlineの部分

inline はinline関数にするための修飾子。

inline

関数型を引数にもつ関数を定義するとき、実行時に無名クラスをたくさん作ってオーバーヘッドを起こさないようにする目的で使用される。inline修飾子をつけておくと、コンパイラがinline宣言されている関数を呼び出しているコードの箇所に直接関数本体を置換する。
(確認したい場合は実際にデコンパイルしてみると良い。)

funの部分

fun は関数宣言。関数を定義することを意味している。

fun


<T>の部分

<T> は型パラメータ。慣習でTという文字が使用されることが多い印象。(Typeの”T”なのかな?と勝手に思っている。知っている人教えてください)

<T>



型パラメータのシンプルな例。クライアントによって、Tは任意の型になる。

sample("hoge") // この場合だと、任意の型TはString型と推論される。
sample(1) // この場合だと、任意の型TはInt型と推論される。

fun <T> sample(t: T) {
    println(t?.hashCode())
}


T.apply()の部分

T.apply() はKotlinの拡張関数。任意の型Tの拡張関数applyを定義するという意味。

T.apply()

拡張関数は[型名].[関数名]という書き方をする。
もちろん拡張関数は、任意の型T出なくても作ることができる。
参考までにString型で拡張関数を作ってみると、こんな感じ。

println("hoge".toQuestion()) // 拡張関数を使って「hoge」を「hoge?」にしてみる

fun String.toQuestion(): String {
    return this + "?"
}


block: T.() -> Unitの部分

block:blockは引数の名前。 T.() -> Unitは関数型。関数型も型の一種で、少し特徴的な印象はあるが、ぜひInt型やString型などと同じくらいの親近感で接してあげたい。
applyという拡張関数はblockという名前の、T.() -> Unitという関数型を引数にとるという意味。

block: T.() -> Unit

関数型は[引数の型] -> [戻り値の型]という書き方をする。

T.() -> Unit

これはT.()という引数をとり、戻り値はないという関数型という意味になる。

では、T.()という引数とは、いったい何者なのか。これは任意の型Tをレシーバとすることを意味している。
レシーバオブジェクトにはthis.でアクセスすることができる。

"hoge".apply({
    this.toUpperCase() // レシーバオブジェクトにはthis.でアクセスできる。
})

突然ラムダ式を記載したが、ラムダは{ }で囲われた処理を関数の引数に渡すことができる。
レシーバ付きラムダは任意のレシーバオブジェクトのメソッドにアクセスできる。

ちなみにここでは明示的に()の中にラムダを書いて、関数の引数に渡したけど、
関数の引数がラムダ唯一であれば、丸括弧()は省略できるので、こう書き換えられる。

"hoge".apply {
    this.toUpperCase()
}


: Tの部分

: Tは関数の返り値。applyという拡張関数は任意の型Tを返すという意味。

: T


ここで一旦整理

ここまでで、一旦整理。

つまりこれ↓は、

public inline fun <T> T.apply(block: T.() -> Unit): T

どこからでもアクセスできる任意の型Tの拡張関数をapplyという名前で、inline関数として定義。
拡張関数の引数にはblockという名前の関数型をとり、その関数型は任意の型Tのレシーバを引数にとり、戻り値はなし。
拡張関数の戻り値は任意の型T

ということ。




続いてapplyの中身、その前に再掲

続いて関数の中身を見ていく。その前にapplyの定義を見る度に、だいぶページをスクロールしないといけなくなるので、復習も兼ねて再掲。

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}


block()の部分

apply 関数に渡された引数の関数オブジェクトを実行している。

block()

apply を使用するときにラムダで関数オブジェクトを渡している。まさにその渡されたラムダ{ }の中身を実行している場所。

"hoge".apply {
    toUpperCase() // ここを実行している
}

Androidアプリではこんな感じで使用することが多い印象。

val bundle = Bundle().apply {
    putString("HOGE", "hoge") // ここを実行している
    putInt("FUGA", 1) // ここを実行している
    putBoolean("IS_PIYO", true) // ここを実行している
}

{ }で囲われた部分が関数オブジェクトとしてapply関数に渡されるので、apply関数の定義に書いてあるblock()では、渡された{ }の中の処理が順番に全部実行されることになる。


return thisの部分

return this は関数の戻り値。つまりここでは、任意の型Tのオブジェクトが返却される。

return this


さいごに

もう一度apply の定義を再掲。

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

どうでしょう??
少しでもコードリーディングを楽しんでいただければ嬉しいです。



ついでに、AndroidアプリでBundle に値を詰め込む実装をapply を使用したときの解説をちょろっとだけ。

val bundle = Bundle().apply {
    putString("HOGE", "hoge")
    putInt("FUGA", 1)
    putBoolean("IS_PIYO", true)
}

apply の定義と照らし合わせると、

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}
  • T …任意の型TBundle
  • block() …関数オブジェクトを実行している部分はputString("HOGE", "hoge”)putInt("FUGA", 1)putBoolean("IS_PIYO", true)
  • return this …返り値の部分は実際に値をputしたBundle

ということ。

参考

いつもお世話になっている本
Kotlin イン・アクション