kotlinの特徴の一つにスコープ関数がありますが、例えばlet
ですとラムダの中でit
で受けたり、run
だとthis
で受けたりしています。どこからこの差異が来るのか不思議になり、実装箇所であるStandard.ktを見ていたら、T.() -> Unit
やら<T>
やらどこかで出会っていながらすんなりと頭に入ってこない表記があったので、自分の中での整理のために考えてみました。
間違っている場合はご指摘いただきますと幸いです。
public inline fun <T> T.apply(block: T.() -> Unit): T
public inline fun <T> T.also(block: (T) -> Unit): T
T.() -> Unit
について
T.() -> Unit
はどういう意味かを考えるために、より具体的にイメージしやすいように、レシーバと引数の二乗和を返す関数sumOfSquares
を実装してみました。
val sumOfSquares: Int.(Int) -> Int = { other -> (this * this).plus(other * other) }
10.sumOfSquares(4) //=> 116
変数sumOfSquares
の型は関数型でInt.(Int) -> Int
は
- レシーバがInt型(一つ目のInt)
- 引数がInt型(二つ目のInt)
- 返り値がInt型(三つ目のInt)
を意味します。大まかには以下と同じです。
fun Int.sumOfSquares(other : Int) = (this * this).plus(other * other)
つまり、apply
の引数定義にあるT.() -> Unit
は
- レシーバがT型
- 引数なし
- 返り値なし
を意味します。apply
はこの三つの条件を満たす関数リテラルを引数としてとるというわけですね。
関数名の前の<T>
について
次に関数名の前についている<T>
ですが、これはジェネリクスを実装する際の書き方になります。なかなか自分でジェネリクスを実装する機会は無いかもしれませんが、ジェネリクスを実装するときは型パラメーターは関数名の前に置きます(参照)。そして、呼び出すときは、関数名の後に型パラメータを置きます。
簡単な例として、オブジェクトを文字列で返すinspect
という関数をジェネリクスで実装してみました。ジェネリクスの実装には、型パラメータを型引数として与えて呼び出す方法と、型パラメータをレシーバとして用いる方法の二つがあります。
- 型パラメータを型引数として用いる例
// サンプルのためのデータクラスでUserを作っておきます。
data class User(var name : String)
fun <T> inspect(item : T): String {
return "$item"
}
inspect<String>("Jiro") //=> Jiro
inspect<User>(User("Jiro")) //=> User(name=Jiro)
上記の例だと型パラメータを型引数として与え、さらに通常の引数を記述することになります。
これは少し面倒です。そこで、オブジェクトそのものにinspect()
というメソッドを生やすことができます。その場合は、拡張関数にする必要があります。
- 型パラメータをメソッドのレシーバとして用いる例
fun <T> T.inspect(): String {
return "$this"
}
"Jiro".inspect() //=> Jiro
User("Jiro").inspect() //=> User(name=Jiro)
null.inspect() //=> null
以上のことを踏まえて、apply
とalso
をみてみます。
applyの場合
public inline fun <T> T.apply(block: T.() -> Unit): T
- Tという型のオブジェクトに
apply
というメソッドを生やす。
2. ここで、TはAny?扱いなので、全てのオブジェクトにメソッドが生えることになる。 - 返り値のオブジェクトの型はT(コードを見ると最後に
return this
している。ここでのthis
はレシーバを指す。) - 引数は関数リテラルで、
4. 引数は、Tという型のオブジェクトの関数で引数は無い関数リテラル
5. 返り値はない(Unit)
ということを意味します。例えば使用例は以下のようになりますが、
// サンプルのためにPersonクラスを簡易的に実装しています
class Person() {
var name : String = ""
}
val user = Person().apply {
name = "Jiro"
}
確かにPerson型のオブジェクトにapplyメソッドが生えており、引数なし・返り値なしの関数リテラルをapplyの引数としてとっています。
ちなみに余談ですが、kotlinの場合、引数の最後のラムダ式を取る場合、括弧を省略できるので上記のような記述ができます。このような書き方ができることがkotlinでDSLを作りやすくしているわけですね。このあたりはrubyとの近しい印象を受けます。
val user = Person().apply({
name = "Jiro"
})
alsoの場合
public inline fun <T> T.also(block: (T) -> Unit): T
- Tという型のオブジェクトに
also
というメソッドを生やす。
2. ここで、TはAny?扱いなので、全てのオブジェクトにメソッドが生えることになる。 - 返り値のオブジェクトの型はT(コードを見ると最後に
return this
している。ここでのthis
はレシーバを指す。) - 引数は関数リテラルで、
4. 引数は、Tという型のオブジェクトを引数に持つ関数リテラル
5. 返り値はない(Unit)
val user = User().also { user ->
user.name = "Jiro"
}
このときalso
の引数の関数リテラル内でのthis
はuser
ではなく外側のものと同じになります。