LoginSignup
811

More than 1 year has passed since last update.

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

Last updated at Posted at 2015-09-29

結論(2021-07-06更新)

個人的なスタンス

個人的な意見として、原則 nullableのためにletを使うのみに留めたいところです。
そもそも、このような記事を書いて、それなりに閲覧されている程度には、スコープ関数は難しいからです。
JavaのAPIを使う場合、alsoapplyが便利な場面があって、これは例外的に使ってもいいかもしれません。
が、スコープ関数を使わない素直なコードの選択肢を常に持っておくべきでしょう。

一応 スコープ関数の図解

Kotlinの概要再確認と2018年の使い方.png

Kotlinの概要再確認と2018年の使い方 (1).png

--------以下、詳しく知りたい人向け--------

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

(2017-01-26 追記) ver1.1で追加されるalsoについて解説を追加しました。
(2017-06-08 追記) 「スコープ関数」という名称の由来について追記しました。

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の場合はこれが必要になる
}

「スコープ関数」という名称について

Kotlin公式ブログの下記エントリにて使用されている「scope functions」が由来です。

What’s new in Standard Library M13 and M14

引用すると

Prior to M13 there were two so-called scope functions in the Standard Library: let and with. We call
them scope functions, because their only purpose is to modify scope of a function passed as the last
parameter.

要するに、letとかの関数の目的は、引数に取る関数のスコープを変更することだから「scope functions」と呼んでいると。

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
811