【Kotlin】let, with, run, apply, alsoの概要と違い、使い分け【スコープ関数解説】
最初に
この記事ではKotlinのlet
, with
, run
, apply
, also
の概要、違い、使い分けについて説明します。
まず初めにこれらが全てスコープ関数であることを挙げ、スコープ関数とは何か説明します。その後、これらの違いと、それぞれの中身について簡単に説明し、最後にどう使い分けたらいいかについて書きました。
手っ取り早く違いや使い分けが知りたい方は違いや使い分けを読んでください。
※ この記事はKotlin公式ドキュメントを日本語訳し、自分なりにまとめたものです。
共通点:スコープ関数
let
, with
, run
, apply
, also
は全て スコープ関数 (Scope Function) と呼ばれる関数です。
スコープ関数とは、「とあるオブジェクトに対してスコープ関数内のコードを実行する」という動作を行う関数です。
例えば以下のコードではlet
を用いてPerson
というオブジェクトに対して{}内の処理を実行しています。
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
Person(name=Alice, age=20, city=Amsterdam)
Person(name=Alice, age=21, city=London)
スコープ関数を使うと 名前無しでオブジェクトにアクセスする ことができます。上記の例の場合、let
なしで書くと
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
となり、alice.XXX
のように書かないといけないところを、スコープ関数を用いるとit.XXX
と書くことができます。(これに関しては次章で詳しく書きます)
単純に文字数が短くなるのはもちろん、スコープ関数を使って{}で囲ってあるほうが「これはPerson("Alice", 20, "Amsterdam")
というオブジェクトに対する処理だ」というのが一目でわかりますね。
このようにスコープ関数は新しい技術を導入するものではありませんが、コードをより簡潔で読みやすくする ことができます。
違い
スコープ関数let
, run
, with
, apply
, also
の主な違いを表にまとめました。
スコープ関数 | オブジェクト参照 | 戻り値 | 拡張関数かどうか |
---|---|---|---|
let |
it |
ラムダ式の戻り値 | ○ |
with |
this |
ラムダ式の戻り値 | ×: 引数でオブジェクトを指定 |
run |
this |
ラムダ式の戻り値 | ○ |
run |
- | ラムダ式の戻り値 | ×: オブジェクト無しで使う |
apply |
this |
オブジェクト自身 | ○ |
also |
it |
オブジェクト自身 | ○ |
各項目について一つ一つ解説していきます。
オブジェクト参照
これは対象のオブジェクトをどうやって参照するかという意味で、this
とit
の2種類があります。this
はラムダのレシーバ、it
はラムダの引数とも呼ばれます。
run
, with
, apply
では this
が使われています。以下の例のようにthis
は省略することができます。
val adam = Person("Adam").apply {
age = 20 // this.age = 20 と同じ
city = "London"
}
println(adam)
省略するとコードを短くできる反面、レシーバのメンバと外部のオブジェクトや関数の区別が難しくなります。上の例で言えば、age
がadam
というオブジェクトのプロパティなのかadam
とは全く関係のない変数なのか分かりにくいということです。
そのためthis
は、対象となるオブジェクトの関数を呼び出したり、プロパティに値を代入したり、オブジェクトのメンバを操作するだけのとき に推奨されます。
一方、let
とalso
ではit
が使われています。this
と違いit
は省略できません。 関数の引数としてそのオブジェクトを指定する場合、オブジェクトのメンバではない変数・関数を使う場合 はit
を使用する方が良いでしょう。
以下の例では、Random.nextInt(100)
(0〜99の乱数)というオブジェクトをit
を使って writeToLog
関数の引数にしています。
Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
INFO: getRandomInt() generated value 71
it
には別の名前をつけることもできます。以下の例ではit
の代わりにvalue
という名前をつけています。
Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
戻り値
スコープ関数の戻り値にはオブジェクト自身を返すものとラムダ式の戻り値を返すものの2種類があります。apply
, also
の戻り値はオブジェクト自身で let
, run
, with
の戻り値はラムダ式の戻り値になります。
具体例を見てみます。「空のリストに要素を2つ追加した後、リストの要素数を出す」という処理をapply
とrun
で書いてみて、出力を比べます。
// 戻り値がオブジェクト自身
val numberList1 = mutableListOf<Int>()
val a = numberList1.apply{
add(1)
add(2)
size
}
println(a)
// 戻り値がラムダ式の戻り値
val numberList2 = mutableListOf<Int>()
val b = numberList2.run{
add(1)
add(2)
size
}
println(b)
[1, 2]
2
apply
は戻り値がオブジェクト自身なので、numberList1
の中身が出力されます。対して run
は戻り値がラムダ式の戻り値です。ラムダ式は{}内の最後のコードの結果が戻り値になるので、size
つまりnumberList2
の要素数である2
が出力されます。
戻り値がオブジェクト自身ある場合、同じオブジェクトに対する関数を何個も繋げることができます。以下の例ではnumberList
にalso
やapply
, sort
を繋げています。
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()
Populating the list
Sorting the list
[1.0, 2.71, 3.14]
戻り値がラムダ式の戻り値の場合、ラムダ式の結果を変数に代入させたり、結果に対して処理を連鎖させたいときに使えます。また、戻り値を無視して、ローカル変数の一時的なスコープとして使うこともできます。
拡張関数
簡単に言うとXXX.let{}
みたいなドットを使った書き方かどうかです。拡張関数ではない場合、例えばwith
はXXX.with{}
と書かずwith(XXX){}
のように書き、対象のオブジェクトを引数で指定します。
let, with, run, apply, also それぞれの概要
let
オブジェクトの参照はit
, 戻り値はラムダ式の戻り値です。そのためlet
はオブジェクトに色々処理を施した後、その結果を引数とした関数を呼び出したいとき に使えます。
以下の例では numbers
というリストに対して map
関数と filter
関数で処理を施した後、その結果を println
関数で出力しています。
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
[5, 4, 4]
let
を使うと、結果を出力するための新たな変数 resultList
を定義する必要なく、簡潔に書くことができます。
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
}
let
の中に関数が1つしかなく、その関数の引数が対象のオブジェクト it
であるとき、メソッド参照(::)を使うこともできます。
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
let
は セーフコール演算子?.
と一緒に使って 「nullになりうる型のオブジェクトに対して、中身がnullではない場合にのみこの処理を実行したい」 というときによく使われます。
以下の例では、変数 str
は String?
型であり、nullの可能性があるため、普通に processNonNullString
関数を呼び出すとコンパイルエラーになります。str?.let {}
と書けばstr
の中身がnullではない場合にのみlet
内の処理を実行することができるので、コンパイルエラーにならず関数を呼び出すことができます。
val str: String? = "Hello"
// processNonNullString(str) // コンパイルエラー: 'str'がnullの可能性もあるため
str?.let {
processNonNullString(it) // OK: '?.let { }'の中では'it'はnullではないことが保証されているため
}
with
オブジェクトの参照はthis
, 戻り値はラムダ式の戻り値です。let
などと違って拡張関数の形を取らず, 引数でオブジェクトを指定します。そのため, with
は返された結果を使う必要がない場合によく使われます。
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
'with' is called with argument [one, two, three]
It contains 3 elements
run
オブジェクトの参照はthis
、 戻り値はラムダ式の戻り値です。基本的に with
と同じですが拡張関数として実装されているため、let
のようにドット記法を使ってオブジェクト上で呼び出すことができます。そのため、run
はオブジェクトの初期化と戻り値の計算の両方を行う場合に便利です。
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
println(result)
Result for query 'Default request to port 8080'
また、拡張関数ではない形で run
を使うこともできます。with
と違って引数にオブジェクトを指定できませんが、ラムダ式の結果は返すので、単純にコードブロックの結果をある変数に代入したいときなどに使えます。
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
println(match.value)
}
+123
-FFFF
88
apply
オブジェクトの参照はthis
、 戻り値はオブジェクト自身で、主にオブジェクトのメンバを操作したいときに使われます。apply
の最も一般的な使用例は、オブジェクトの設定です。
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
also
オブジェクトの参照はit
、 戻り値はオブジェクト自身です。
also
はオブジェクトを引数にとるアクションを実行するときにも便利です。オブジェクトのプロパティや関数ではなく、オブジェクト自体への参照が必要なアクションや、スコープ外から参照したくない場合に使います。
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
使い分け
以上より、各スコープ関数には大きくオブジェクトの参照がthis
かit
か、戻り値がオブジェクト自身かラムダ式の戻り値かという大きく2つの違いがあります。
オブジェクトの参照について補足すると
-
this
は省略可能なのでオブジェクトのメンバだけを操作したいとき向け - 逆に
it
は省略できないので、関数の引数としてそのオブジェクトを指定する場合、オブジェクトのメンバではない変数・関数を使うとき向け
という話でした。
これらの違いを踏まえ、以下のような使い分けができると思います。
let
: non-nullなオブジェクトに対してコードを実行したいとき
with
: 返された結果を使う必要がないとき(オブジェクトに対する関数呼び出しのグループ化)
run
: オブジェクトの初期化と戻り値の計算の両方を行いたいとき
run
(拡張関数じゃないとき) : 単純にコードブロックの結果をある変数に代入したいとき
apply
: オブジェクトの設定
also
: オブジェクトを引数にとるアクションを実行するとき、特にオブジェクト自体への参照が必要なアクションのとき
ただし、スコープ関数に明確な違いはありません。大抵の場合、とあるスコープ関数で書かれた処理は別のスコープ関数で書き直すことができるので、上記のショートガイドやあなたのプロジェクトで使われている規則などに基づいて何を使うか選んでください。
まとめ
このブログでは、kotlinのスコープ関数let
, with
, run
, apply
, also
についてその概要や違い、使い分けについて説明しました。
-
let
,with
,run
,apply
,also
は全てはスコープ関数と呼ばれる関数で、特定のオブジェクトに対する処理を実行するためのもの - 主な違いは①オブジェクト参照②戻り値の2つ
- 上記の違いに基づいた使い分けは一応できるものの、これらに明確な違いはないので、現場の雰囲気や思想で使い分けよう
Discussion