🐤

【Kotlin】let, with, run, apply, alsoの概要と違い、使い分け【スコープ関数解説】

2023/07/02に公開

最初に

この記事では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 オブジェクト自身

各項目について一つ一つ解説していきます。

オブジェクト参照

これは対象のオブジェクトをどうやって参照するかという意味で、thisitの2種類があります。thisはラムダのレシーバ、itはラムダの引数とも呼ばれます。

run, with, apply では this が使われています。以下の例のようにthisは省略することができます。

val adam = Person("Adam").apply { 
    age = 20                       // this.age = 20 と同じ
    city = "London"
}
println(adam)

省略するとコードを短くできる反面、レシーバのメンバと外部のオブジェクトや関数の区別が難しくなります。上の例で言えば、ageadamというオブジェクトのプロパティなのかadamとは全く関係のない変数なのか分かりにくいということです。
そのためthisは、対象となるオブジェクトの関数を呼び出したり、プロパティに値を代入したり、オブジェクトのメンバを操作するだけのとき に推奨されます。

一方、letalsoでは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つ追加した後、リストの要素数を出す」という処理をapplyrunで書いてみて、出力を比べます。

// 戻り値がオブジェクト自身
 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が出力されます。

戻り値がオブジェクト自身ある場合、同じオブジェクトに対する関数を何個も繋げることができます。以下の例ではnumberListalsoapply , 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{}みたいなドットを使った書き方かどうかです。拡張関数ではない場合、例えばwithXXX.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ではない場合にのみこの処理を実行したい」 というときによく使われます。
以下の例では、変数 strString? 型であり、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")

使い分け

以上より、各スコープ関数には大きくオブジェクトの参照がthisitか、戻り値がオブジェクト自身かラムダ式の戻り値かという大きく2つの違いがあります。

オブジェクトの参照について補足すると

  • thisは省略可能なのでオブジェクトのメンバだけを操作したいとき向け
  • 逆にitは省略できないので、関数の引数としてそのオブジェクトを指定する場合、オブジェクトのメンバではない変数・関数を使うとき向け

という話でした。

これらの違いを踏まえ、以下のような使い分けができると思います。

let : non-nullなオブジェクトに対してコードを実行したいとき
with : 返された結果を使う必要がないとき(オブジェクトに対する関数呼び出しのグループ化)
run : オブジェクトの初期化と戻り値の計算の両方を行いたいとき
run(拡張関数じゃないとき) : 単純にコードブロックの結果をある変数に代入したいとき
apply : オブジェクトの設定
also : オブジェクトを引数にとるアクションを実行するとき、特にオブジェクト自体への参照が必要なアクションのとき

ただし、スコープ関数に明確な違いはありません。大抵の場合、とあるスコープ関数で書かれた処理は別のスコープ関数で書き直すことができるので、上記のショートガイドやあなたのプロジェクトで使われている規則などに基づいて何を使うか選んでください。

まとめ

このブログでは、kotlinのスコープ関数let, with, run, apply, alsoについてその概要や違い、使い分けについて説明しました。

  • let, with, run, apply, alsoは全てはスコープ関数と呼ばれる関数で、特定のオブジェクトに対する処理を実行するためのもの
  • 主な違いは①オブジェクト参照②戻り値の2つ
  • 上記の違いに基づいた使い分けは一応できるものの、これらに明確な違いはないので、現場の雰囲気や思想で使い分けよう

Discussion