- Kotlin
Kotlinラムダ式と高階関数の書き方 it・関数型・::・inline
Kotlin のラムダ式とは、名前を付けずにその場で書ける関数で、波かっこの中に「引数 -> 処理」の形で書きます。高階関数とは、関数を引数に受け取る、または関数を戻り値として返す関数です。 filter や map にラムダ式を渡す書き方が、その代表例です。
Kotlin のラムダ式は省略できる箇所が多く、同じ処理に何通りもの書き方があります。この記事では、完全な形から1段ずつ縮めていく省略ステップ表を最初に置き、どの段階でも同じ結果になることを実行して示します。続けて、関数型の書き方、:: による関数参照の4種類、ラムダの中の return、inline・noinline・crossinline を、エラー文つきで順に確かめます。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。掲載した出力とエラー文は、すべてこの環境でコンパイル・実行して得たものです。
ラムダ式の省略ステップ早見表
「6文字以上の単語だけを残す」という同じ処理を、上から順に縮めていきます。
| 段階 | 書き方 | 省いたもの | 省ける条件 |
|---|---|---|---|
| 1. 完全な形 | words.filter({ word: String -> word.length >= 6 }) |
なし | ― |
| 2. 型推論 | words.filter({ word -> word.length >= 6 }) |
引数の型 | 受け取る側の関数型から型がわかる |
| 3. 末尾ラムダ | words.filter() { word -> word.length >= 6 } |
かっこの中に書くこと | ラムダが最後の引数 |
| 4. かっこの省略 | words.filter { word -> word.length >= 6 } |
空の () |
ラムダ以外の引数がない |
| 5. it | words.filter { it.length >= 6 } |
引数名と -> |
引数が1つだけ |
| 6. 関数参照 | words.filter(::isLong) |
ラムダそのもの | 同じ引数・戻り値の関数がすでにある |
どの段階で書いても結果は同じです。 ふだんは 4 か 5 の形で書き、処理に名前が付いた関数があるときは 6 を使う、という使い分けが読みやすくなります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlinラムダ式と高階関数の書き方 it・関数型・::・inline」
まず動かす:6つの書き方が同じ結果になることを確かめる
次のコードを Main.kt という名前で保存し、そのままコンパイルして実行できます。
fun isLong(word: String): Boolean = word.length >= 6
fun main() {
val words = listOf("apple", "banana", "kiwi", "orange")
// 1. 完全な形:引数の型を書き、かっこの中に渡す
println(words.filter({ word: String -> word.length >= 6 }))
// 2. 型推論:引数の型を省く
println(words.filter({ word -> word.length >= 6 }))
// 3. 末尾ラムダ:最後の引数のラムダをかっこの外へ
println(words.filter() { word -> word.length >= 6 })
// 4. かっこが空になったら、かっこごと省く
println(words.filter { word -> word.length >= 6 })
// 5. it:引数が1つなら名前と -> を省く
println(words.filter { it.length >= 6 })
// 6. 関数参照:既にある関数を :: で渡す
println(words.filter(::isLong))
}
kotlinc Main.kt -include-runtime -d main.jar
java -Dstdout.encoding=UTF-8 -jar main.jar
-Dstdout.encoding=UTF-8 は、日本語の出力を UTF-8 で表示するための指定です。
[banana, orange]
[banana, orange]
[banana, orange]
[banana, orange]
[banana, orange]
[banana, orange]
6行とも同じ結果になりました。
ラムダ式の形
{ 引数1, 引数2 -> 処理 }
- 波かっこで全体を囲みます。 引数は
->の左、処理は右に書きます。 - 最後に書いた式の値が、ラムダの戻り値になります。return は書きません。
- 引数がないときは
{ println("hello") }のように、->ごと省きます。
ラムダ式の書き方いろいろ:複数引数・分解・クロージャ
inline fun <reified T> List<*>.countOf(): Int = count { it is T }
fun counter(): () -> Int {
var count = 0 // ラムダが外側の変数をつかまえる(クロージャ)
return { ++count }
}
fun main() {
// 外側の var を書き換えられる
var sum = 0
listOf(1, 2, 3).forEach { sum += it }
println(sum)
val next = counter()
println("${next()} ${next()} ${next()}")
// 分解宣言と、使わない引数の _
val stock = mapOf("pen" to 12, "note" to 0)
stock.forEach { (name, qty) -> println("$name=$qty") }
stock.forEach { (name, _) -> print("$name ") }
println()
// 引数が複数のラムダ
println(listOf(3, 1, 2).sortedWith { a, b -> b - a })
println(listOf(1, 2, 3).fold(0) { acc, n -> acc + n })
// ラムダを2つ受け取る関数:末尾に出せるのは最後の1つ
fun <T> T.check(ok: (T) -> Boolean, orElse: () -> T): T = if (ok(this)) this else orElse()
println(5.check({ it > 10 }) { 10 })
// 入れ子のラムダでは it に名前を付ける
val table = listOf(listOf(1, 2), listOf(3, 4))
println(table.map { row -> row.map { cell -> cell * 10 } })
// 無名関数:戻り値の型を書ける
val half = fun(n: Int): Double = n / 2.0
println(half(5))
// reified:inline 関数では型引数を実行時にも使える
println(listOf(1, "a", 2, "b", 3.0).countOf<String>())
// 型を書かずに変数へ入れるときは、引数の型をラムダ側に書く
val square = { n: Int -> n * n }
println(square(4))
}
6
1 2 3
pen=12
note=0
pen note
[3, 2, 1]
6
10
[[10, 20], [30, 40]]
2.5
2
16
| やりたいこと | 書き方 | 補足 |
|---|---|---|
| 外側の変数を使う・書き換える | forEach { sum += it } |
ラムダは外側の var も変更できる(クロージャ) |
| 状態を持つ関数を返す | return { ++count } |
返したラムダが count を持ち続ける |
| 引数を分解して受ける | { (name, qty) -> … } |
丸かっこで囲む。Map の要素や data class に使える |
| 使わない引数 | { (name, _) -> … } |
_ と書くと名前を付けずに済む |
| 引数が2つ以上 | { a, b -> b - a } |
it は使えないので名前を付ける |
| ラムダを2つ渡す | x.check({ … }) { … } |
かっこの外に出せるのは最後の1つ |
| 入れ子 | { row -> row.map { cell -> … } } |
it が重なる場所では名前を付けると読みやすい |
| 戻り値の型を書きたい | fun(n: Int): Double = n / 2.0 |
無名関数。ラムダと同じ場所に渡せる |
| 型を書かずに変数へ入れる | val square = { n: Int -> n * n } |
引数の型をラムダの中に書く |
型を推論できないときのエラー文
引数の型を省けるのは、受け取る側の型から決まるときだけです。手がかりがない場合や、引数の数が合わない場合は次のようになります。
fun main() {
val f = { a, b -> a + b }
val g: (Int) -> Int = { a, b -> a + b }
val h: (Int, Int) -> Int = { it * 2 }
}
Infer.kt:2:15: error: an explicit type is required on a value parameter.
val f = { a, b -> a + b }
^
Infer.kt:2:18: error: an explicit type is required on a value parameter.
val f = { a, b -> a + b }
^
Infer.kt:3:25: error: initializer type mismatch: expected '(Int) -> Int', actual '(Int, ??? (Unknown type for value parameter b)) -> Int'.
val g: (Int) -> Int = { a, b -> a + b }
^
Infer.kt:3:32: error: cannot infer type for value parameter 'b'. Specify it explicitly.
val g: (Int) -> Int = { a, b -> a + b }
^
Infer.kt:4:30: error: initializer type mismatch: expected '(Int, Int) -> Int', actual '() -> Int'.
val h: (Int, Int) -> Int = { it * 2 }
^
Infer.kt:4:34: error: unresolved reference 'it'.
val h: (Int, Int) -> Int = { it * 2 }
^^
| エラー文 | 原因 | 直し方 |
|---|---|---|
an explicit type is required on a value parameter |
型の手がかりがない | { a: Int, b: Int -> a + b } と書くか、変数に型を付ける |
initializer type mismatch: expected '(Int) -> Int' … |
引数の数が関数型と合わない | 関数型かラムダのどちらかを直す |
unresolved reference 'it' |
引数が2つ以上のラムダで it を使った | { a, b -> … } と名前を付ける |
高階関数と関数型の書き方一覧
関数を引数や戻り値にするには、その「型」を書く必要があります。これが関数型です。
| 関数型 | 意味 | ラムダの例 |
|---|---|---|
(Int) -> String |
Int を1つ受け取り String を返す | { n -> "No.$n" } |
(Int, Int) -> Int |
引数2つ | { a, b -> a + b } |
() -> Unit |
引数なし、戻り値なし(Unit は省けない) | { println("hello") } |
(index: Int, line: String) -> Unit |
引数に名前を付けた形(説明用) | { i, line -> … } |
((String) -> Unit)? |
関数そのものが null かもしれない | null または { … } |
(String) -> Int? |
戻り値が null かもしれない | { it.toIntOrNull() } |
StringBuilder.() -> Unit |
レシーバ付き。ラムダの中で this が StringBuilder | { append("a") } |
suspend (Int) -> String |
中断できる関数 | { attempt -> … } |
(Int) -> (Int) -> Int |
関数を返す関数 | { a -> { b -> a + b } } |
typealias Validator = (String) -> Boolean |
関数型に名前を付ける | val v: Validator = { it.isNotBlank() } |
null 許容の位置に注意します。 ((String) -> Unit)? は関数全体が null 許容、(String) -> Int? は戻り値だけが null 許容です。
動かして確かめる
// 高階関数:関数を引数に取る
fun calc(a: Int, b: Int, op: (Int, Int) -> Int): Int = op(a, b)
// 高階関数:関数を戻り値にする
fun multiplier(n: Int): (Int) -> Int = { it * n }
// 引数名つきの関数型(名前は説明用)
fun eachLine(text: String, action: (index: Int, line: String) -> Unit) {
text.lines().forEachIndexed { i, line -> action(i, line) }
}
// null 許容の関数型:かっこで囲んでから ?
fun notify(message: String, onDone: ((String) -> Unit)? = null) {
println("送信: $message")
onDone?.invoke("完了")
}
// レシーバ付き関数型:ラムダの中で this が StringBuilder になる
fun buildText(block: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.block()
return sb.toString()
}
// suspend 付き関数型
suspend fun retry(times: Int, block: suspend (attempt: Int) -> String): String {
var last = ""
for (i in 1..times) {
last = block(i)
if (last == "OK") break
}
return last
}
// 型に名前を付ける
typealias Validator = (String) -> Boolean
suspend fun main() {
println(calc(6, 3) { a, b -> a + b })
println(calc(6, 3, Int::times))
val triple = multiplier(3)
println(triple(5))
println(multiplier(10)(5))
eachLine("alpha\nbeta") { i, line -> println("$i: $line") }
notify("注文を受け付けました")
notify("発送しました") { println("コールバック: $it") }
println(buildText {
append("Hello")
append(", ")
append("Kotlin")
})
println(retry(3) { attempt -> if (attempt < 2) "NG" else "OK" })
val notBlank: Validator = { it.isNotBlank() }
val short: Validator = { it.length <= 5 }
val rules: List<Validator> = listOf(notBlank, short)
println(rules.all { it("Kotlin") })
println(rules.map { it("abc") })
// 変数に入れたラムダは () でも invoke でも呼べる
val add: (Int, Int) -> Int = { a, b -> a + b }
println(add(1, 2))
println(add.invoke(1, 2))
// 引数なし、戻り値なし
val hello: () -> Unit = { println("hello") }
hello()
}
9
18
15
50
0: alpha
1: beta
送信: 注文を受け付けました
送信: 発送しました
コールバック: 完了
Hello, Kotlin
OK
false
[true, true]
3
3
hello
- 関数を引数に取る:calc の3番目の引数 op が関数型です。ラムダでも
Int::timesのような関数参照でも渡せます。 - 関数を返す:multiplier(3) は「3倍する関数」を返します。
multiplier(10)(5)のように続けて呼ぶこともできます。 - null 許容の関数型:
onDone?.invoke("完了")と、安全呼び出しと invoke を組み合わせて呼びます。 - レシーバ付き:buildText のラムダの中では、
appendを対象の指定なしで呼べます。apply や with、buildString が同じ仕組みです。 - suspend:suspend 付きの関数型は、suspend 関数の中から呼びます。この例では main を
suspend fun main()にしています。 - 呼び出し方:変数に入れた関数は
add(1, 2)でもadd.invoke(1, 2)でも呼べます。
関数参照 :: の4種類
すでにある関数を渡すときは、ラムダで包まずに ::(コロン2つ) で参照を渡せます。
| 種類 | 書き方 | 関数型 | 同じ意味のラムダ |
|---|---|---|---|
| トップレベル関数 | ::shout |
(String) -> String |
{ shout(it) } |
| メンバー関数(クラス名から) | User::isAdult |
(User) -> Boolean |
{ it.isAdult() } |
| コンストラクタ | ::User |
(String, Int) -> User |
{ n, a -> User(n, a) } |
| 束縛参照(インスタンスから) | greeter::greet |
(String) -> String |
{ greeter.greet(it) } |
| プロパティ | User::name |
(User) -> String |
{ it.name } |
data class User(val name: String, val age: Int) {
fun isAdult(): Boolean = age >= 18
}
fun shout(s: String): String = s.uppercase() + "!"
class Greeter(private val greeting: String) {
fun greet(name: String): String = "$greeting, $name"
}
fun main() {
val names = listOf("aoi", "ren")
val users = listOf(User("aoi", 17), User("ren", 30))
// 1. トップレベル関数の参照 ::関数名
println(names.map(::shout))
// 2. メンバー関数の参照 クラス名::関数名(第1引数がそのクラスのインスタンス)
println(users.filter(User::isAdult))
println(names.map(String::length))
// 3. コンストラクタの参照 ::クラス名
val pairs = listOf("mio" to 20, "sora" to 15)
println(pairs.map { (n, a) -> User(n, a) })
val create: (String, Int) -> User = ::User
println(create("kai", 41))
// 4. 束縛参照 インスタンス::関数名(対象のインスタンスが決まっている)
val greeter = Greeter("こんにちは")
println(names.map(greeter::greet))
val printer: (Any?) -> Unit = ::println
printer("println も参照で渡せる")
// プロパティの参照も同じ形で書ける
println(users.map(User::name))
println(users.sortedByDescending(User::age).map(User::name))
// 参照の型を確かめる
val f1: (String) -> String = ::shout
val f2: (User) -> Boolean = User::isAdult // レシーバが第1引数になる
val f3: (String) -> String = greeter::greet // レシーバは束縛済み
println(f1("a") + " " + f2(users[1]) + " " + f3("x"))
}
[AOI!, REN!]
[User(name=ren, age=30)]
[3, 3]
[User(name=mio, age=20), User(name=sora, age=15)]
User(name=kai, age=41)
[こんにちは, aoi, こんにちは, ren]
println も参照で渡せる
[aoi, ren]
[ren, aoi]
A! true こんにちは, x
クラス名から参照するか、インスタンスから参照するかで、関数型が変わります。
User::isAdultは対象のインスタンスが決まっていないので、第1引数として User を受け取る(User) -> Booleanになります。greeter::greetは対象が greeter に決まっているので、残りの引数だけの(String) -> Stringになります。
ラムダと関数参照の使い分け
| 状況 | 選ぶもの |
|---|---|
| 渡したい処理が、既存の関数1つの呼び出しそのもの | 関数参照(map(String::length)) |
| プロパティを取り出すだけ | どちらでも。map { it.name } も短く読みやすい |
| 引数を加工してから渡す、複数の処理をつなぐ | ラムダ |
| 同じ名前の関数が複数あり(オーバーロード)、どれを指すか読み取りにくい | ラムダ |
ラムダの中の return:エラー文と4つの抜け方
ラムダの中に return をそのまま書くと、コンパイルエラーになります。
fun runTwice(action: (Int) -> Unit) {
action(1)
action(2)
}
fun main() {
runTwice { n ->
if (n == 1) return
println("n=$n")
}
println("end")
}
Return.kt:8:21: error: 'return' is prohibited here.
if (n == 1) return
^^^^^^
Kotlin の return は「一番内側の fun から抜ける」という意味です。ラムダは fun ではないので、ここでの return は main から抜けることを指します。ところが runTwice は渡されたラムダをあとで呼ぶだけの通常の関数で、ラムダの中から main を終わらせる方法がありません。そのためコンパイラが止めます。
やりたいことに応じて、次の4つの書き方があります。
fun runTwice(action: (Int) -> Unit) {
action(1)
action(2)
}
inline fun runTwiceInline(action: (Int) -> Unit) {
action(1)
action(2)
}
fun labeled() {
runTwice { n ->
if (n == 1) return@runTwice // このラムダの今回の呼び出しだけを終える
println("labeled: n=$n")
}
println("labeled: end")
}
fun customLabel() {
runTwice skip@{ n ->
if (n == 1) return@skip // 自分で付けたラベルも使える
println("customLabel: n=$n")
}
println("customLabel: end")
}
fun anonymous() {
runTwice(fun(n: Int) {
if (n == 1) return // 無名関数の return は無名関数だけを終える
println("anonymous: n=$n")
})
println("anonymous: end")
}
fun nonLocal() {
runTwiceInline { n ->
if (n == 2) return // inline なので外側の nonLocal() ごと終わる
println("nonLocal: n=$n")
}
println("nonLocal: end") // ここには来ない
}
fun findFirstEven(list: List<Int>): Int? {
list.forEach { // forEach は inline 関数
if (it % 2 == 0) return it // findFirstEven から値を返して抜ける
}
return null
}
fun lastValue(): String {
val f = { n: Int ->
if (n > 0) "plus" else "zero or minus" // 最後の式がラムダの戻り値
}
return f(3)
}
fun main() {
labeled()
customLabel()
anonymous()
nonLocal()
println(findFirstEven(listOf(1, 3, 4, 6)))
println(lastValue())
println(listOf(1, 2, 3).map { if (it == 2) return@map 0; it * 10 })
}
labeled: n=2
labeled: end
customLabel: n=2
customLabel: end
anonymous: n=2
anonymous: end
nonLocal: n=1
4
plus
[10, 0, 30]
| 書き方 | 抜ける範囲 | 出力で確かめたこと |
|---|---|---|
return@runTwice |
ラムダの今回の呼び出しだけ(ループでいう continue に近い) | n=1 を飛ばして n=2 と end が出る |
skip@{ … return@skip } |
同上。自分でラベルを付ける | 同じ結果 |
無名関数 fun(n: Int) { return } |
無名関数だけ | 同じ結果 |
inline 関数に渡したラムダの return |
外側の関数ごと(非ローカル return) | n=1 のあと、end が出ずに終わる |
return@map 0 |
ラムダの戻り値を指定して終える | 2 のところだけ 0 になる |
ラベル名は、ラムダを渡した関数の名前です。forEach に渡したラムダなら return@forEach、map なら return@map と書きます。
forEach・map・filter・let・run などの標準ライブラリの関数は inline なので、その中の return は外側の関数から抜けます。findFirstEven の return it が、ループの途中で関数全体の結果を返せているのはこのためです。
inline・noinline・crossinline の違い
inline:ラムダを呼び出し元に展開する
通常の高階関数では、ラムダは関数オブジェクトとして作られ、それを経由して呼び出されます。関数に inline を付けると、コンパイラが関数本体とラムダの中身を呼び出し元に埋め込みます。Kotlin 公式ドキュメントでは、関数オブジェクトの生成と呼び出しにかかる実行時のコストを減らせることが、inline の目的として説明されています。
埋め込まれた結果、ラムダの中身は「外側の関数の一部」になります。先ほどの非ローカル return が書けるのは、この仕組みのためです。
一方で、埋め込むことによる制約もあります。inline 関数の引数のラムダを、変数に入れたり、別のラムダの中から呼んだりしてみます。
inline fun measure(label: String, block: () -> Unit) {
val saved: () -> Unit = block
saved()
}
inline fun later(block: () -> Unit): Runnable {
return Runnable { block() }
}
fun main() {
measure("a") { println("a") }
later { println("b") }.run()
}
Inline.kt:2:29: error: illegal usage of inline parameter 'block: () -> Unit'. Add 'noinline' modifier to the parameter declaration.
val saved: () -> Unit = block
^^^^^
Inline.kt:7:23: error: cannot inline 'block: () -> Unit' here: it might contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block: () -> Unit'.
return Runnable { block() }
^^^^^
エラー文が、付けるべき修飾子を教えてくれます。
- 1つ目:展開されるラムダは「物」として存在しないので、変数に入れられません。→ noinline を付ける
- 2つ目:block の中に return が書かれていた場合、Runnable の中からでは外側の関数を終わらせられません。→ crossinline を付ける
noinline と crossinline を付けて動かす
// noinline:その引数だけインライン展開せず、関数オブジェクトとして扱う
inline fun measure(label: String, noinline block: () -> Unit) {
val saved: () -> Unit = block // 変数に入れられる
saved()
println("$label: done")
}
// crossinline:別のラムダやオブジェクトの中から呼べる。代わりに非ローカル return を禁じる
inline fun later(crossinline block: () -> Unit): Runnable {
return Runnable { block() }
}
fun main() {
measure("a") { println("a") }
later { println("b") }.run()
}
Main.kt:2:1: warning: expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of function types.
inline fun measure(label: String, noinline block: () -> Unit) {
^^^^^^
a
a: done
b
どちらもコンパイルが通り、動きました。measure に出ている warning は「展開されるラムダ引数が1つもないので、inline にする効果が小さい」という知らせです。noinline は、ラムダ引数が複数あって、その一部だけを変数に入れたい・他の関数に渡したい、というときに使う修飾子だとわかります。
noinline・crossinline を付けた引数では return が書けない
inline fun measure(label: String, noinline block: () -> Unit) {
block()
}
inline fun later(crossinline block: () -> Unit): Runnable {
return Runnable { block() }
}
fun main() {
measure("a") { return }
later { return }.run()
}
InlineReturn.kt:9:20: error: 'return' is prohibited here.
measure("a") { return }
^^^^^^
InlineReturn.kt:10:13: error: 'return' is prohibited here.
later { return }.run()
^^^^^^
どちらも非ローカル return は使えなくなります。return@measure、return@later のラベル付きなら書けます。
3つの修飾子の比較表
| 修飾子 | 付ける場所 | ラムダの扱い | 変数に入れる・他へ渡す | 別のラムダやオブジェクトの中から呼ぶ | ラムダ内の return |
|---|---|---|---|---|---|
| (inline 関数の通常の引数) | ― | 呼び出し元に展開 | できない | できない | 書ける(外側の関数から抜ける) |
noinline |
引数 | 展開しない(関数オブジェクト) | できる | できる | 書けない |
crossinline |
引数 | 展開する | できない | できる | 書けない |
| (inline でない関数の引数) | ― | 関数オブジェクト | できる | できる | 書けない |
inline を付けるかどうかの目安
| 状況 | 目安 |
|---|---|
| ラムダを受け取る小さな関数で、ループの中などから何度も呼ばれる | inline が向いている |
| ラムダを受け取らない関数 | 付けても効果は小さい(上の warning が出る) |
| 本体が長い関数 | 呼び出し箇所ごとにコードが増えるので、付けずにおく |
型引数を実行時に使いたい(it is T など) |
inline と reified を組み合わせる(countOf の例) |
つまずきやすい点の一覧
| 起きること | 表示(今回の環境での文言) | 対応 |
|---|---|---|
| ラムダの中に return を書いた | 'return' is prohibited here. |
return@関数名 にする。値は最後の式で返す |
| forEach の中の return で関数ごと終わった | (エラーではない) | 次の要素へ進みたいなら return@forEach |
| 引数2つのラムダで it を使った | unresolved reference 'it'. |
{ a, b -> … } と名前を付ける |
| 型なしの変数にラムダを入れた | an explicit type is required on a value parameter. |
ラムダの引数に型を書く |
| inline 関数の引数を変数に入れた | illegal usage of inline parameter … Add 'noinline' modifier … |
引数に noinline を付ける |
| inline 関数の引数を別のラムダの中で呼んだ | cannot inline … it might contain non-local returns. Add 'crossinline' modifier … |
引数に crossinline を付ける |
null 許容の関数型を f() で呼んだ |
reference has a nullable type '(() -> Unit)?'. Use explicit '?.invoke' to make a function-like call instead. |
f?.invoke() と書く |
よくある質問
Q. it はいつ使えますか?
A. ラムダの引数が1つだけのときです。引数名と -> を省くと、その引数を it という名前で使えます。引数が2つ以上のとき、または引数がないときは使えません。ラムダを入れ子にする場合は、it が重なって読み取りにくくなるので、名前を付けるのがおすすめです。
Q. ラムダ式と無名関数はどう違いますか?
A. 無名関数は fun(n: Int): Double { … } のように fun で書く、名前のない関数です。戻り値の型を書けることと、中の return が無名関数だけを終えることが、ラムダとの違いです。渡せる場所は同じです。
Q. forEach の中で次の要素へ進むにはどう書きますか?
A. return@forEach と書きます。ラベルなしの return は forEach を呼んでいる関数ごと終えます。途中で繰り返し全体を止めたいときは、for 文に break を書く形が読みやすくなります。
Q. inline はすべての高階関数に付けたほうがよいですか? A. ラムダを受け取る小さな関数に向いています。本体が長い関数に付けると、呼び出し箇所ごとにコードが展開されて量が増えます。また、引数のラムダを変数に入れる・別のラムダの中から呼ぶ場合は、noinline や crossinline の指定が必要になります。
まとめ
- ラムダ式は { 引数 -> 処理 }。最後の式が戻り値になる
- 省略は5段階:引数の型 → かっこの外へ → 空のかっこ → it → 関数参照。どれも同じ結果になる
- 関数型は (A) -> B。null 許容は全体をかっこで囲んで ?、レシーバ付きは A.() -> B、中断できる関数は suspend を前に置く
- :: は4種類:トップレベル(::f)、メンバー(User::f)、コンストラクタ(::User)、束縛参照(obj::f)。クラス名からの参照は第1引数が対象になる
- ラムダの中の return は 'return' is prohibited here。return@ラベルで今回の呼び出しだけを終える
- inline 関数に渡したラムダでは return が書け、外側の関数から抜ける。forEach や map がこれに当たる
- noinline は「展開しない」、crossinline は「展開するが非ローカル return を禁じる」。どちらが必要かはエラー文が教えてくれる
参考・出典
- Kotlin 公式ドキュメント「Higher-order functions and lambdas」(関数型の書き方、末尾ラムダ、it、無名関数、クロージャ): https://kotlinlang.org/docs/lambdas.html
- Kotlin 公式ドキュメント「Inline functions」(inline の目的、noinline、crossinline、非ローカル return、reified): https://kotlinlang.org/docs/inline-functions.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlinラムダ式と高階関数の書き方 it・関数型・::・inline」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。