29
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

kotlinのT.() -> Unitや<T>を理解する

Last updated at Posted at 2018-07-28

kotlinの特徴の一つにスコープ関数がありますが、例えばletですとラムダの中でitで受けたり、runだとthisで受けたりしています。どこからこの差異が来るのか不思議になり、実装箇所であるStandard.ktを見ていたら、T.() -> Unitやら<T>やらどこかで出会っていながらすんなりと頭に入ってこない表記があったので、自分の中での整理のために考えてみました。
間違っている場合はご指摘いただきますと幸いです。

apply
public inline fun <T> T.apply(block: T.() -> Unit): T
also
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

  1. レシーバがInt型(一つ目のInt)
  2. 引数がInt型(二つ目のInt)
  3. 返り値がInt型(三つ目のInt)

を意味します。大まかには以下と同じです。

fun Int.sumOfSquares(other : Int) = (this * this).plus(other * other)

つまり、applyの引数定義にあるT.() -> Unit

  1. レシーバがT型
  2. 引数なし
  3. 返り値なし

を意味します。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

以上のことを踏まえて、applyalsoをみてみます。

applyの場合

apply
public inline fun <T> T.apply(block: T.() -> Unit): T
  1. Tという型のオブジェクトにapplyというメソッドを生やす。
    2. ここで、TはAny?扱いなので、全てのオブジェクトにメソッドが生えることになる。
  2. 返り値のオブジェクトの型はT(コードを見ると最後にreturn thisしている。ここでのthisはレシーバを指す。)
  3. 引数は関数リテラルで、
    4. 引数は、Tという型のオブジェクトの関数で引数は無い関数リテラル
    5. 返り値はない(Unit)

ということを意味します。例えば使用例は以下のようになりますが、

kotlin
// サンプルのために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の場合

also
public inline fun <T> T.also(block: (T) -> Unit): T 
  1. Tという型のオブジェクトにalsoというメソッドを生やす。
    2. ここで、TはAny?扱いなので、全てのオブジェクトにメソッドが生えることになる。
  2. 返り値のオブジェクトの型はT(コードを見ると最後にreturn thisしている。ここでのthisはレシーバを指す。)
  3. 引数は関数リテラルで、
    4. 引数は、Tという型のオブジェクトを引数に持つ関数リテラル
    5. 返り値はない(Unit)
val user = User().also { user ->
    user.name = "Jiro"
}

このときalsoの引数の関数リテラル内でのthisuserではなく外側のものと同じになります。

参考

29
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?