Home About Contact
Kotlin , Kotlin Exposed

Kotlin の apply の T.()->Unit について調べた

Kotlin の apply の型のシグニチャーはこれ:

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

この apply に適用する block の型 T.() -> Unit はいったい何?これが今まで何なのかよくわからなった。 apply 自体はこの意味が分からなくても普通に使える。

たとえば、Button クラスについて考えてみる。

class Button {
  var label: String = ""
  var foregroundColor: Color = Color.BLACK
  var backgroundColor: Color = Color.WHITE

  override fun toString(): String {
    return "button ${label}, ${foregroundColor}, ${backgroundColor}"
  }
}

補足 データクラスを使えばもう少しシンプルにこの Button クラスを記述できます。

data class Button(
  var label: String,
  var foregroundColor: Color,
  var backgroundColor: Color) {

  override fun toString(): String {
    return "button ${label}, ${foregroundColor}, ${backgroundColor}"
  }
}

ただ、個人的にはデータクラスのプロパティが書き換え可能というのは全く好きではない。

apply を使わないで、このボタンを初期化するとしたら次のようなコードになるだろう。

val button = Button()
button.label = "Click me"
button.foregroundColor = Color.BLUE
button.backgroundColor = Color.YELLOW

println(button)

実行してみる。

$ kotlin button.main.kts
button Click me, java.awt.Color[r=0,g=0,b=255], java.awt.Color[r=255,g=255,b=0]

これを apply を使って書けば、次のようになる。

val button = Button().apply {
  label = "Click me"
  foregroundColor = Color.BLUE
  backgroundColor = Color.YELLOW
}

println(button)

実行してみると同じ結果になります。

block: T.() -> Unit

先ほどの Button().apply 部分を 丸括弧つきで 冗長に記述すれば次のようになる。

val button = Button().apply( {
  label = "Click me"
  foregroundColor = Color.BLUE
  backgroundColor = Color.YELLOW
} )

したがって、この apply の型シグニチャー:

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

block に相当するのは次の部分になります。

{
  label = "Click me"
  foregroundColor = Color.BLUE
  backgroundColor = Color.YELLOW
}

ならばこの blockapply とは切り離して記述することもできるのか? このように:

val block = {
  label = "Click me"
  foregroundColor = Color.BLUE
  backgroundColor = Color.YELLOW
}

Button().apply( block )

これを実行するとコンパイラーに怒られます。 次のようなエラーです。

button.main.kts:37:3: error: unresolved reference 'label'.
  label = "Click me"
  ^

確かに。現状の val block の型は一見 ()->Unit のように見えますが、 そのブロックの中には label, foregroundColor, backgroundColor プロパティが記述されている。 そのすぐあとで Button().apply(block) しているので、コードを書いている側にしてみたら自明ですけど。 コンパイラにしたら、それどこのプロパティなんだ?ってことになりますね、たぶん。

それで block に明示的に型 Button.() -> Unit を指定してやります。

val block: Button.() -> Unit = {
  label = "Click me"
  foregroundColor = Color.BLUE
  backgroundColor = Color.YELLOW
}

val button = Button().apply( block )

T.() -> Unit の T の部分に Button という具体的なクラス名を指しているだけです。 これで意図通り Button のプロパティをセットアップするブロックを apply とは別に記述できるようになりました。

また、blockthis を使って次のように冗長に書くこともできます。

val block: Button.() -> Unit = {
  this.label = "Click me"
  this.foregroundColor = Color.BLUE
  this.backgroundColor = Color.YELLOW
}

つまり、 Button.() と記述するとブロック内では Button クラスのインスタンスを this として参照できるようになる ということです。ちょっと興味深いですね。

set がプロパティになる

T.() -> Unit と直接関係はないのですが、Kotlin には fun set["foo"] = bar と記述できる(シンタックスシュガーなのかな) フィーチャーがあります。

例のための例になりますが、先ほどの Button クラスをこのフィーチャーを無理やり使えるように記述を修正してみます。

class Button {
  private var label: String = ""
  private var foregroundColor: Color = Color.BLACK
  private var backgroundColor: Color = Color.WHITE

  operator fun set(key: String, value: String): Unit{
    if( key=="label" ){
      label = value
    }
  }
  
  operator fun set(key: String, value: Color): Unit{
    if( key=="foregroundColor" ){
      foregroundColor = value
    }
    if( key=="backgroundColor" ){
      backgroundColor = value
    }
  }

  override fun toString(): String {
    return "button ${label}, ${foregroundColor}, ${backgroundColor}"
  }
}

このフィーチャーを使うには fun set の前に operator を追加する必要があります。

まずはこのクラスを初期化する block を普通に set を使って記述します。

val block: Button.() -> Unit = {
  set("label", "Click me")
  set("foregroundColor", Color.BLUE)
  set("backgroundColor", Color.YELLOW)
}

冗長に this.set として記述しても同じことです。

val block: Button.() -> Unit = {
  this.set("label", "Click me")
  this.set("foregroundColor", Color.BLUE)
  this.set("backgroundColor", Color.YELLOW)
}

そして、これができるならば次のようにも記述できます。

val block: Button.() -> Unit = {
  this["label"] = "Click me"
  this["foregroundColor"] = Color.BLUE
  this["backgroundColor"] = Color.YELLOW
}

別に set を使えば済む話なのですが、 set を使った Java風コードではなく、 DSL的に定義の集まりとしてコードを表現するには好都合です。

まとめ

今回完成したコードを掲載。

// button.main.kts

import java.awt.Color

class Button {
  private var label: String = ""
  private var foregroundColor: Color = Color.BLACK
  private var backgroundColor: Color = Color.WHITE

  operator fun set(key: String, value: String): Unit{
    if( key=="label" ){
      label = value
    }
  }

  operator fun set(key: String, value: Color): Unit{
    if( key=="foregroundColor" ){
      foregroundColor = value
    }
    if( key=="backgroundColor" ){
      backgroundColor = value
    }
  }

  override fun toString(): String {
    return "button ${label}, ${foregroundColor}, ${backgroundColor}"
  }
}

val block: Button.() -> Unit = {
  this["label"] = "Click me"
  this["foregroundColor"] = Color.BLUE
  this["backgroundColor"] = Color.YELLOW
}

val button = Button().apply( block )
println(button)

以上です。