1. Qiita
  2. Items
  3. Kotlin

Kotlin スコープ関数 用途まとめ

  • 160
    Like
  • 0
    Comment

Kotlinの標準ライブラリには「スコープ関数」と呼ばれる4つの関数があります。
letwithrunapplyです。
それぞれ似ているので使い分けが難しいと思い、私なりの考えをまとめておきます。

(2017-01-26 追記) ver1.1で追加されるalsoについて解説を追加しました。

let

定義

public inline fun <T, R> T.let(f: (T) -> R): R = f(this)

使用例

val s = "hoge".let { it.toUpperCase() }
println(s) //=> HOGE

定義からもわかりますがletは任意の型の拡張関数です。
上記の例ではStringインスタンス"hoge"をレシーバとしています。
letは引数として関数を取ります。
この関数はletのレシーバを受け取り、任意の型を返します。

主な用途

nullableな変数に対して使うことが多いと思います。
nullableと組み合わせて使うことによりletはJavaのOptionalにおける次の3つのメソッドの役割と似ています。

  • map
  • flatMap
  • ifPresent

例えばString?な変数fooの大文字表現を得たい場合にはこう記述できます。

val upperCase: String? = foo?.let { it.toUpperCase() }

foonullである場合には?.呼び出しによりletを実行せずnullを返します。
foonullでない場合にはletが実行されit.toUpperCase()によりfooの大文字表現が返されます。

蛇足ですが、上記のようなケースではfoo?.let(String::toUpperCase)と記述できます。

with

定義

public inline fun <T, R> with(receiver: T, f: T.() -> R): R = receiver.f()

使用例

val s = with("hoge") { this.toUpperCase() }
println(s) //=> HOGE

withletとは異なり拡張関数ではありません。
第一引数に任意の型Tを取ります。
第二引数に関数を取りますが、Tをレシーバとするメソッドである必要があります。
上記の例における{ this.toUpperCase() }this"hoge"を指します。
もちろんthisは省略可能なので{ toUpperCase() }と記述してもOKです。

主な用途

あるインスタンスに対する複数の操作をすっきり記述することが目的だと思われます。
例えばインスタンスを生成し、各種設定を行ってから実際に使用するという場面は多いです。

val frame: JFrame = with(JFrame("My App")) {
  size = Dimension(600, 400)
  defaultCloseOperation = JFrame.EXIT_ON_CLOSE
  setVisible(true)
  this
}

sizeプロパティやsetVisibleメソッドはJFrameのメンバです。
つまりこの文脈でレシーバはthisとなり、省略可能なのでレシーバを繰り返し記述する必要がなくなります。

この用途のためにletを使うことは適していません。
なぜならsizesetVisibleにアクセスする際にはレシーバとしてitが必須でありit.sizeit.setVisible(true)のように記述する必要があるからです。

上記の例のようにwithの引数として渡したインスタンスをごにょごにょした後に、そのインスタンス自体を返す場合は後述のapplyの方が適しているかも知れません。

run

定義

public inline fun <T, R> T.run(f: T.() -> R): R = f()

使用例

val s = "hoge".run { toUpperCase() }
println(s) //=> HOGE

letwithが合わさったような定義になっています。
runは任意の型Tの拡張関数で、そのTをレシーバとするメソッドのような関数を引数に取ります。

主な用途

用途もletwithの合体版だと思います。

val frame: JFrame? = frameOrNull()
frame?.run {
  size = Dimension(600, 400)
  defaultCloseOperation = JFrame.EXIT_ON_CLOSE
  setVisible(true)
}

apply

定義

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

使用例

val s = "hoge".apply { toUpperCase() }
println(s) //=> hoge

今回は他の使用例と変わり、変数sは小文字のままです。
applyは引数として受け取った関数を実行して何か仕事をしますが、返す値はレシーバだからです。

主な用途

withの、レシーバを返して欲しいときに使う版と言えます。
withを使用したコードと見比べてください。

val frame: JFrame = JFrame("My App").apply {
  size = Dimension(600, 400)
  defaultCloseOperation = JFrame.EXIT_ON_CLOSE
  setVisible(true)
}

withを使用したバージョンでは、引数の関数内の最後に戻り値としてthisと明記していました。
applyはレシーバを返すのでthisと明記する手間が省けます。

Android開発者にはおなじみだと思いますが、
Fragmentをnewして、argumentsをセットするパターンを実装する際に効果を発揮します。

apply未使用
class MyFragment: Fragment() {
  companion object {
    fun new(foo: Int, bar: Int): MyFragment {
      val args = Bundle()
      args.putInt("foo", foo)
      args.putInt("bar", bar)
      val myFragment = MyFragment()
      myFragment.arguments = args
      return myFragment
    }
  }
}
apply使用
class MyFragment: Fragment() {
  companion object {
    fun new(foo: Int, bar: Int): MyFragment =
      MyFragment().apply {
        arguments = Bundle().apply {
          putInt("foo", foo)
          putInt("bar", bar)
        }
      }
  }
}

実際のプロダクトでは

手元のKotlinソースコードが7000行ほどのAndroidアプリで、どの程度スコープ関数が使われているか調べてみました。

関数 使用回数
let 54
with 0
run 40
apply 37

withは1回も使ってませんが、それ以外は、けっこう使ってるんですね。
スコープ関数の便利さをお伝えできれば嬉しいです。

also

ver 1.1から標準ライブラリに加わる拡張関数です。

定義 

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

使用例と主な用途

単純な例
val s = "hoge".also { it.toUpperCase() }
println(s) //=> hoge

定義や上記の例からもわかるように、applyとほぼ同じです。異なる点は「引数が元のレシーバ("hoge")の拡張関数ではなく、元のレシーバを引数に取る関数である」というところです。その利点は2つあります。
まず、名前を付けられることです。引数の関数をラムダ式として記述した場合、元のレシーバをitとして参照できますが、何か意味のある名前を付けてやれば(例えばnameとかlabelとか)、コードの可読性が上がるでしょう。
そしてもう一つの利点は、ラムダ式の内と外でthisの意味が変わらないということです。

applyを使用
val button = Button(this).apply {
  text = "Click me"
  setOnClickListener { 
    startActivity(Intent(this@MainActivity, NextActivity::class.java))
    // 単なる「this」ではNG   ^
  }
}
alsoを使用
val button = Button(this).also { button -> 
  button.text = "Click me"
  button.setOnClickListener { 
    startActivity(Intent(this, NextActivity::class.java))
  }
}

同じく「元のレシーバを引数に取る関数」を引数に取るletとの違いは、letは戻り値を自分で決めるのに対し、alsoは元のレシーバが返されるという点です。

letを使用
val button = Button(this).let { button -> 
  button.text = "Click me"
  button.setOnClickListener { 
    startActivity(Intent(this, NextActivity::class.java))
  }
  button // letの場合はこれが必要になる
}