- Kotlin
Kotlin関数の書き方 fun・デフォルト引数・名前付き引数・vararg
Kotlin の関数は、fun 関数名(引数名: 型): 戻り値の型 { 本体 } の形で書きます。 本体が式1つなら = でつないで波かっこと return を省け、引数には既定値(デフォルト引数)を付けられ、呼び出すときは引数名を指定(名前付き引数)できます。
この記事では、fun の書き方を7種類の早見表にまとめ、すべてのコードに実行結果を付けました。さらに、デフォルト引数と名前付き引数を混ぜるときの順番のルール、vararg に配列を渡すスプレッド演算子、ブロック本体で戻り値の型を省いたときの動きなど、最初に引っかかりやすい点を実際のコンパイルエラー文つきで再現しています。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。掲載した出力とエラー文は、すべてこの環境でコンパイル・実行して得たものです。
fun の書き方の早見表
| 種類 | 書き方 | 呼び出し | 結果 |
|---|---|---|---|
| ブロック本体 | fun add(a: Int, b: Int): Int { return a + b } |
add(2, 3) |
5 |
| 単一式関数 | fun double(x: Int) = x * 2 |
double(21) |
42 |
| Unit(返す値なし) | fun greet(name: String) { println(...) } |
greet("田中") |
表示だけ行い、戻り値は kotlin.Unit |
| Nothing(戻らない) | fun stop(message: String): Nothing { throw ... } |
name ?: stop("...") |
例外を投げて終わる |
| ローカル関数 | 関数の中に fun format(p: Int) = ... |
外側の関数の中だけ | 1,200円 / 35,000円 |
| infix(中置) | infix fun Int.times(text: String) = ... |
3 times "ab" |
ababab |
| tailrec(末尾再帰) | tailrec fun sumTo(n: Long, acc: Long = 0): Long = ... |
sumTo(1_000_000) |
500000500000 |
引数まわりの書き方は次の4つです。
| やりたいこと | 宣言 | 呼び出し | 覚えておく点 |
|---|---|---|---|
| 既定値を持たせる | count: Int = 1 |
order("ノート") |
省略した引数に既定値が入る |
| 名前で渡す | (宣言は通常どおり) | order("ノート", gift = true) |
途中の引数を飛ばしたら、それ以降は名前を付ける |
| 個数を決めずに受け取る | vararg nums: Int |
total(1, 2, 3) |
関数の中では配列(IntArray や Array)として扱う |
| 配列を vararg に渡す | (同上) | total(*arr) |
配列の前に *(スプレッド演算子)を付ける |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin関数の書き方 fun・デフォルト引数・名前付き引数・vararg」
まず動かす:7種類の fun を1本のコードで
次のコードを Basic.kt という名前で保存します。早見表の7種類がすべて入っています。
// 1. ブロック本体:戻り値型を書き、return で返す
fun add(a: Int, b: Int): Int {
return a + b
}
// 2. 単一式関数:= の右に式を1つ。戻り値型は推論される
fun double(x: Int) = x * 2
// 3. Unit:返す値がない関数。「: Unit」は省略できる
fun greet(name: String) {
println("こんにちは、${name}さん")
}
// 4. Nothing:必ず例外を投げ、呼び出し元に戻らない関数
fun stop(message: String): Nothing {
throw IllegalStateException(message)
}
// 5. ローカル関数:関数の中だけで使う関数。外側の変数を読める
fun priceLabel(prices: List<Int>): String {
val unit = "円"
fun format(p: Int) = "%,d%s".format(p, unit)
return prices.joinToString(" / ") { format(it) }
}
// 6. infix:引数1つのメンバー関数・拡張関数を「a 関数名 b」で呼べる
infix fun Int.times(text: String) = text.repeat(this)
// 7. tailrec:末尾再帰をループに変換する
tailrec fun sumTo(n: Long, acc: Long = 0): Long =
if (n == 0L) acc else sumTo(n - 1, acc + n)
fun main() {
println(add(2, 3))
println(double(21))
val r = greet("田中")
println(r)
println(priceLabel(listOf(1200, 35000)))
println(3 times "ab")
println(sumTo(1_000_000))
val name: String? = null
val n = name ?: stop("name が null です")
println(n)
}
コンパイルして実行します。-include-runtime は Kotlin の標準ライブラリを jar に含める指定、-Dstdout.encoding=UTF-8 は日本語の出力を UTF-8 にそろえる指定です。
kotlinc Basic.kt -include-runtime -d basic.jar
java -Dstdout.encoding=UTF-8 -jar basic.jar
5
42
こんにちは、田中さん
kotlin.Unit
1,200円 / 35,000円
ababab
500000500000
Exception in thread "main" java.lang.IllegalStateException: name が null です
at BasicKt.stop(Basic.kt:16)
at BasicKt.main(Basic.kt:42)
at BasicKt.main(Basic.kt)
読み取れることは3つあります。
greetの戻り値を変数に入れて表示すると kotlin.Unit と出ます。返す値がない関数も、Unit という値を1つ返しています。sumTo(1_000_000)は100万回の再帰ですが、tailrec を付けているので最後まで計算できます。- 最後の4行は
stopが例外を投げた結果です。name ?: stop(...)の右側が実行され、その後ろのprintln(n)には進みません。
ここから、それぞれの書き方を順に見ていきます。
ブロック本体と単一式関数:戻り値の型を書くとき・省けるとき
| 本体の形 | 戻り値の型 | return |
|---|---|---|
ブロック本体 { ... } で値を返す |
必ず書く | 必要 |
ブロック本体 { ... } で値を返さない |
省略できる(Unit になる) | 不要 |
単一式 = 式 |
省略できる(式から推論される) | 書かない |
Kotlin の公式ドキュメントも、ブロック本体の関数は戻り値の型を必ず明示し、例外は Unit を返すときだけ、と説明しています。
ブロック本体で戻り値の型を省くと Unit になる
Java や他の言語に慣れていると、戻り値の型を書き忘れることがあります。次のコードはコンパイルエラーになります。
// E5.kt
fun area(w: Int, h: Int) {
return w * h
}
fun main() {
val a: Int = area(3, 4)
}
E5.kt:3:12: error: return type mismatch: expected 'Unit', actual 'Int'.
return w * h
^^^^^
E5.kt:7:16: error: initializer type mismatch: expected 'Int', actual 'Unit'.
val a: Int = area(3, 4)
^
型を書いていないブロック本体は Unit を返す関数として扱われます。そのため「Unit のはずなのに Int を返している」と「Int の変数に Unit を入れようとしている」の2つが報告されます。fun area(w: Int, h: Int): Int { と型を足せば解決します。
return を書かなかった場合はコンパイルが通ってしまうので、さらに気づきにくくなります。次のコードで確かめます。
// = と { } を両方書くと「ラムダを返す関数」になる
fun hello() = { println("hello") }
// ブロック本体で戻り値型を省くと Unit。最後の式は返らない
fun area(w: Int, h: Int) {
w * h
}
fun explicitUnit(): Unit {
return Unit
}
fun main() {
hello() // 何も表示されない(ラムダを作って捨てている)
hello()() // ラムダを呼ぶと表示される
println(area(3, 4))
println(explicitUnit())
val f: () -> Unit = ::explicitUnit // 関数型では Unit を省略できない
println(f())
}
hello
kotlin.Unit
kotlin.Unit
kotlin.Unit
area(3, 4)は 12 ではなく kotlin.Unit です。ブロックの最後にw * hと書いても、ブロック本体では return を書かないかぎり値は返りません。fun hello() = { ... }は「ラムダ(関数の値)を返す関数」です。hello()だけでは何も表示されず、hello()()と呼んではじめて中身が動きます。=と{ }はどちらか一方だけを書きます。
| 書いたもの | 意味 |
|---|---|
fun f() { println("hi") } |
表示する関数(Unit を返す) |
fun f() = println("hi") |
同じく表示する関数(単一式) |
fun f() = { println("hi") } |
「表示するラムダ」を返す関数 |
単一式関数で型を書いたほうがよい場面
単一式関数は型を省けますが、自分自身を呼ぶ再帰関数では推論できません。
// E6.kt
fun fact(n: Int) = if (n <= 1) 1 else n * fact(n - 1)
fun main() {
println(fact(5))
}
E6.kt:2:43: error: type checking has run into a recursive problem. Easiest workaround: specify the types of your declarations explicitly.
fun fact(n: Int) = if (n <= 1) 1 else n * fact(n - 1)
^^^^^^^^^^^
メッセージのとおり、fun fact(n: Int): Int = ... と型を書けばコンパイルできます。ほかにも、公開する関数(ライブラリの API など)は、本体を書き換えたときに戻り値の型が意図せず変わらないよう、型を明示しておくと安心です。
Unit:返す値がない関数の戻り値
Unit は「値が1つだけある型」です。Java の void に近い役割ですが、Unit は実際の型なので、変数に入れたりジェネリクスの型引数に使ったりできます。
| 書き方 | 説明 |
|---|---|
fun log(msg: String) { ... } |
いちばん普通の書き方。: Unit は省略 |
fun log(msg: String): Unit { ... } |
省略しない書き方。意味は同じ |
return または return Unit |
どちらも書ける。書かなくてもよい |
val f: () -> Unit = ::log のような関数型 |
関数型では Unit を省略できない |
先ほどの UnitPitfall.kt の explicitUnit() と f() が、どちらも kotlin.Unit を表示していたのがこの動きです。
デフォルト引数:既定値を持たせる
引数の型の後ろに = 値 を書くと、呼び出すときにその引数を省略できます。
fun order(item: String, count: Int = 1, gift: Boolean = false, note: String = "なし") {
println("item=$item count=$count gift=$gift note=$note")
}
fun main() {
order("ノート") // 既定値をすべて使う
order("ノート", 3) // 位置で2つ目まで
order("ノート", gift = true) // 途中を飛ばすときは名前を付ける
order(note = "急ぎ", item = "ペン") // 名前付きなら順番は自由
order("ペン", count = 2, true) // 名前付きでも位置が合っていれば続けて位置で書ける
order(item = "ペン", 5, gift = true, "包装") // 位置どおりなら混在できる
}
item=ノート count=1 gift=false note=なし
item=ノート count=3 gift=false note=なし
item=ノート count=1 gift=true note=なし
item=ペン count=1 gift=false note=急ぎ
item=ペン count=2 gift=true note=なし
item=ペン count=5 gift=true note=包装
既定値には式が書ける(呼ぶたびに評価される)
既定値は固定の値だけでなく、関数呼び出しや、前にある引数を使った式も書けます。式は関数を呼ぶたびに評価されます。
var counter = 0
fun nextId(): Int { counter++; return counter }
// 既定値には式が書ける。前の引数も使える。呼ぶたびに評価される
fun register(name: String, id: Int = nextId(), label: String = "$name#$id") {
println(label)
}
fun main() {
register("A")
register("B")
register("C", id = 100)
register("D")
}
A#1
B#2
C#100
D#3
register("C", id = 100) のときは nextId() が呼ばれないので、次の D は 3 番になります。既定値の式は、その引数が省略されたときだけ動きます。
既定値のある引数を前に置いたとき
既定値のある引数を、既定値のない引数より前に置くこともできます。その場合、既定値を使うには後ろの引数を名前付きで渡します。
fun greeting(userId: Int = 0, message: String) {
println("userId=$userId message=$message")
}
// 最後の引数がラムダなら、既定値を飛ばして波かっこの外に書ける
fun retry(times: Int = 3, action: (Int) -> Unit) {
for (i in 1..times) action(i)
}
fun main() {
greeting(message = "Hello!")
greeting(7, "Hi!")
retry { println("try $it") }
retry(1) { println("once $it") }
}
userId=0 message=Hello!
userId=7 message=Hi!
try 1
try 2
try 3
once 1
greeting("Hello!") と位置だけで書くと、"Hello!" は1つ目の userId に渡そうとしたことになり、次のエラーになります。
// E2.kt
fun greeting(userId: Int = 0, message: String) {}
fun main() {
greeting("Hello!")
}
E2.kt:5:5: error: no value passed for parameter 'message'.
greeting("Hello!")
^^^^^^^^
E2.kt:5:14: error: argument type mismatch: actual type is 'String', but 'Int' was expected.
greeting("Hello!")
^^^^^^^^
例外は最後の引数がラムダの場合です。retry { ... } のように波かっこを丸かっこの外に書くと、それは必ず最後の引数に対応するので、前にある times の既定値がそのまま使われます。
設計の目安としては、必ず渡す引数を前、既定値のある引数を後ろに並べると、位置だけでも名前付きでも呼びやすくなります。
名前付き引数:混ぜて書くときのルール
呼び出すときに 引数名 = 値 と書くのが名前付き引数です。true や数値が並ぶ呼び出しでも、何を渡しているのかが読み取れるようになります。
| 書き方 | 結果 | 理由 |
|---|---|---|
order("ノート", 3) |
通る | すべて位置どおり |
order("ノート", gift = true) |
通る | count を飛ばし、以降は名前付き |
order(note = "急ぎ", item = "ペン") |
通る | すべて名前付きなら順番は自由 |
order("ペン", count = 2, true) |
通る | count が本来の位置にあるので、続きを位置で書ける |
order(item = "ペン", 5, gift = true, "包装") |
通る | 名前付きの引数がすべて本来の位置にある |
order("ペン", gift = true, 2) |
エラー | gift が本来の位置(3番目)ではなく2番目にあり、その後ろに位置引数がある |
最後の行を実際にコンパイルすると、次のメッセージが出ます。
// E1.kt
fun order(item: String, count: Int = 1, gift: Boolean = false, note: String = "なし") {}
fun main() {
order("ペン", gift = true, 2)
}
E1.kt:5:30: error: mixing named and positional arguments is not allowed unless the order of the arguments matches the order of the parameters.
order("ペン", gift = true, 2)
^
メッセージは「名前付き引数と位置引数は、引数の並びが宣言の順番と一致しているときだけ混ぜられる」という意味です。迷ったときは、「引数を1つでも飛ばしたら、そこから後ろはすべて名前を付ける」と覚えておけば、このエラーにはなりません。
名前付き引数に関するほかのエラー文もまとめて確認しました。order は fun order(item: String, count: Int = 1) と宣言しています。
| 書いた呼び出し | エラー文 | 直し方 |
|---|---|---|
order("ペン", count = 2, count = 3) |
argument already passed for this parameter. | 同じ引数を2回渡さない |
order(name = "ペン") |
no parameter with name 'name' found. | 宣言にある引数名(item)を使う |
order() |
no value passed for parameter 'item'. | 既定値のない引数は必ず渡す |
order("ペン", 1, 2) |
too many arguments for 'fun order(item: String, count: Int = ...): Unit'. | 引数の数を宣言に合わせる |
なお、関数をオーバーライドするときは、子クラス側に既定値を書きません。親の既定値がそのまま使われます。
open class Shape {
open fun draw(width: Int = 10, height: Int = 5) = println("Shape ${width}x$height")
}
class Rect : Shape() {
// override 側には既定値を書かない。親の既定値が使われる
override fun draw(width: Int, height: Int) = println("Rect ${width}x$height")
}
fun main() {
Rect().draw()
Rect().draw(height = 2)
}
Rect 10x5
Rect 10x2
子クラス側に width: Int = 20 と書くと、an overriding function is not allowed to specify default values for its parameters. というエラーになります。
vararg:引数の個数を決めずに受け取る
引数に vararg を付けると、0個以上の値をカンマ区切りで渡せるようになります。関数の中では配列として扱います。
fun total(vararg nums: Int): Int {
println("型=${nums::class.simpleName} 個数=${nums.size}")
return nums.sum()
}
fun tags(vararg names: String) = names.joinToString(",", "[", "]")
// vararg を先頭に置いた場合、後ろの引数は名前付きで渡す
fun log(vararg parts: String, sep: String = " ") = parts.joinToString(sep)
fun <T> asList(vararg ts: T): List<T> = ts.toList()
fun main() {
println(total())
println(total(1, 2, 3))
val arr = intArrayOf(10, 20, 30)
println(total(*arr)) // IntArray はそのまま * で展開
println(total(1, *arr, 99)) // 前後に値を足せる
val list = listOf("kotlin", "jvm")
println(tags(*list.toTypedArray())) // List は配列に変換してから *
println(log("a", "b", "c"))
println(log("a", "b", "c", sep = "-"))
val a = intArrayOf(1, 2, 3)
println(asList(-1, 0, *a.toTypedArray(), 4)) // ジェネリクスの vararg は Array<T>
println(asList(a).size) // * なしだと IntArray 1個のリスト
}
型=IntArray 個数=0
0
型=IntArray 個数=3
6
型=IntArray 個数=3
60
型=IntArray 個数=5
160
[kotlin,jvm]
a b c
a-b-c
[-1, 0, 1, 2, 3, 4]
1
vararg nums: Int の実体は IntArray で、引数を1つも渡さなければ個数 0 の配列になります。vararg names: String なら Array<String> です。
配列を渡すときはスプレッド演算子を付ける
すでに配列を持っているときに、そのまま渡すとエラーになります。
// E4.kt
fun total(vararg nums: Int) = nums.sum()
fun tags(vararg names: String) = names.joinToString()
fun main() {
val arr = intArrayOf(10, 20, 30)
println(total(arr))
val list = listOf("kotlin", "jvm")
println(tags(*list))
}
E4.kt:7:19: error: argument type mismatch: actual type is 'IntArray', but 'Int' was expected.
println(total(arr))
^^^
E4.kt:9:19: error: argument type mismatch: actual type is 'List<String>', but 'Array<out String>' was expected.
println(tags(*list))
^^^^
1つ目は「Int を1個ずつ受け取る場所に IntArray を渡した」、2つ目は「* で展開できるのは配列で、List は展開できない」という意味です。直し方は次のとおりです。
| 持っているもの | 渡し方 | 説明 |
|---|---|---|
IntArray(vararg nums: Int へ) |
total(*arr) |
配列の前に * を付けて展開する |
List<String>(vararg names: String へ) |
tags(*list.toTypedArray()) |
List を配列に変換してから * |
IntArray(ジェネリクスの vararg ts: T へ) |
asList(*a.toTypedArray()) |
Array<Int> に変換してから * |
| 配列+追加の値 | total(1, *arr, 99) |
展開の前後に値を並べられる |
3行目は見落としやすい点です。ジェネリクスの vararg は Array<T> を受け取るため、プリミティブ型の配列である IntArray をそのまま *a で渡すと、次のエラーになります。
E10.kt:8:35: error: argument type mismatch: actual type is 'IntArray', but 'Array<out Int>' was expected.
val list: List<Int> = asList(*a)
^
また、asList(a) のように * を付けずに渡すとコンパイルは通りますが、「IntArray が1個入ったリスト」になります(上の実行結果の最後の行で、size が 1 になっています)。
vararg の後ろの引数は名前付きで渡す
vararg は最後の引数に置くのが基本です。途中や先頭に置いた場合、その後ろの引数は名前付きで渡します。位置だけで書くと、すべて vararg 側に取り込まれるためです。
// E3.kt
fun join(vararg parts: String, sep: String) = parts.joinToString(sep)
fun main() {
println(join("a", "b", "-"))
}
E3.kt:5:13: error: no value passed for parameter 'sep'.
println(join("a", "b", "-"))
^^^^
join("a", "b", sep = "-") と書けば通ります。Vararg.kt の log のように後ろの引数に既定値を付けておくと、log("a", "b", "c") と log("a", "b", "c", sep = "-") の両方で呼べます。
vararg を付けられる引数は、1つの関数につき1つだけです。2つ書くと multiple vararg parameters are prohibited. というエラーになります。
Nothing:呼び出し元に戻らない関数
戻り値の型 Nothing は、「この関数は必ず例外を投げるなどして、呼び出し元に戻らない」ことを表します。
fun fail(message: String): Nothing = throw IllegalArgumentException(message)
fun parsePort(text: String): Int {
// toIntOrNull() が null のとき fail() に進む。fail は戻らないので port は Int になる
val port: Int = text.toIntOrNull() ?: fail("数値ではありません: $text")
return port
}
fun notYet(): String = TODO("あとで書く")
fun main() {
println(parsePort("8080"))
try {
parsePort("abc")
} catch (e: IllegalArgumentException) {
println("捕まえた: ${e.message}")
}
try {
notYet()
} catch (e: NotImplementedError) {
println(e.message)
}
}
8080
捕まえた: 数値ではありません: abc
An operation is not implemented: あとで書く
text.toIntOrNull() ?: fail(...) の右側が Nothing なので、コンパイラは「ここを通過できたなら左側は null ではない」と判断し、port を Int として扱えます。標準ライブラリの TODO() も Nothing を返す関数で、まだ書いていない本体の仮置きに使えます。
| 型 | 意味 | 関数が終わったあと |
|---|---|---|
| Unit | 返す値がない | 呼び出し元の次の行へ進む |
| Nothing | そもそも戻らない | 次の行へは進まない |
ローカル関数:関数の中だけで使う関数
関数の中にも fun を書けます。外側の関数の変数をそのまま読み書きできるので、その関数の中でだけ繰り返す処理をまとめるのに向いています。
fun validate(name: String, email: String): List<String> {
val errors = mutableListOf<String>()
// ローカル関数:外側の errors に直接追加できる
fun requireNotBlank(label: String, value: String) {
if (value.isBlank()) errors += "$label は必須です"
}
requireNotBlank("名前", name)
requireNotBlank("メール", email)
return errors
}
fun main() {
println(validate("田中", ""))
println(validate("", " "))
println(validate("田中", "t@example.com"))
}
[メール は必須です]
[名前 は必須です, メール は必須です]
[]
requireNotBlank は validate の外からは見えません。ファイル全体やクラス全体から呼ぶ必要のない小さな処理は、ローカル関数にしておくと名前が外に出ず、読む範囲が狭くなります。
infix:a 関数名 b の形で呼ぶ
infix を付けた関数は、ドットとかっこを省いて a 関数名 b の形で呼べます。条件は次の3つです。
- クラスのメンバー関数か、拡張関数である
- 引数がちょうど1つである
- その引数は vararg ではなく、既定値も持たない
data class Version(val major: Int, val minor: Int) {
infix fun isNewerThan(other: Version) =
major > other.major || (major == other.major && minor > other.minor)
}
infix fun String.joinWith(other: String) = "$this-$other"
fun main() {
val a = Version(2, 4)
val b = Version(2, 1)
println(a isNewerThan b) // 中置で呼ぶ
println(a.isNewerThan(b)) // 通常の呼び方も同じ
println("a" joinWith "b" joinWith "c")
println(1 to "one") // 標準ライブラリの to も infix 関数
println(1 + 2 shl 2) // 算術演算子が先:(1 + 2) shl 2
println(1 shl 2 + 1) // 1 shl (2 + 1)
}
true
true
a-b-c
(1, one)
12
8
1 to "one" で Pair を作る to も、標準ライブラリの infix 関数です。最後の2行は優先順位の確認で、中置呼び出しは算術演算子(+ など)より後に評価されます。1 + 2 shl 2 は (1 + 2) shl 2 で 12、1 shl 2 + 1 は 1 shl (2 + 1) で 8 です。
条件を満たさない関数に infix を付けると、'infix' modifier is inapplicable to this function. というエラーになります(引数が2つの拡張関数と、拡張ではないトップレベル関数の両方で確認しました)。
tailrec:末尾再帰をループに変える
再帰関数は、呼び出しが深くなるとスタック(呼び出しの記録を積む領域)を使い切ります。tailrec を付けると、コンパイラが再帰をループに変換するため、深さを気にせず書けます。
// tailrec なし:呼び出しのたびにスタックを積む
fun sumPlain(n: Long, acc: Long = 0): Long =
if (n == 0L) acc else sumPlain(n - 1, acc + n)
// tailrec あり:コンパイラがループに変換する
tailrec fun sumLoop(n: Long, acc: Long = 0): Long =
if (n == 0L) acc else sumLoop(n - 1, acc + n)
// 再帰呼び出しのあとに掛け算が残るので、末尾呼び出しにならない
tailrec fun fact(n: Long): Long =
if (n <= 1) 1 else n * fact(n - 1)
fun main() {
println(sumLoop(1_000_000))
try {
println(sumPlain(1_000_000))
} catch (e: StackOverflowError) {
println("sumPlain: ${e::class.simpleName}")
}
println(fact(10))
}
コンパイルすると、fact に対して警告が2つ出ます。
Tailrec.kt:10:1: warning: a function is marked as tail-recursive but no tail calls are found.
tailrec fun fact(n: Long): Long =
^^^^^^^
Tailrec.kt:11:28: warning: recursive call is not a tail call.
if (n <= 1) 1 else n * fact(n - 1)
^^^^
実行結果は次のとおりです。
500000500000
sumPlain: StackOverflowError
3628800
sumLoopは100万回の再帰でも計算できます。- 同じ内容で tailrec のない
sumPlainは StackOverflowError で止まります。 factは、再帰呼び出しの結果にn *を掛ける処理が残っているので、末尾呼び出しになりません。警告が出て、通常の再帰としてコンパイルされます。
tailrec を使えるのは、自分自身の呼び出しが関数の最後の処理であるときです。公式ドキュメントでは、再帰呼び出しのあとに処理が残る場合、try / catch / finally の中、open な関数では使えないと説明されています。sumLoop のように途中経過を引数(acc)に持たせる形に書き換えると、末尾呼び出しにできます。
Java のオーバーロードとの対比と @JvmOverloads
Java にはデフォルト引数がないため、引数の数ごとにメソッドを並べます。
public class Mailer {
// Java は引数の数ごとにメソッドを並べる(オーバーロード)
static String send(String to) {
return send(to, "(件名なし)");
}
static String send(String to, String subject) {
return send(to, subject, false);
}
static String send(String to, String subject, boolean urgent) {
return "to=" + to + " subject=" + subject + " urgent=" + urgent;
}
public static void main(String[] args) {
System.out.println(send("a@example.com"));
System.out.println(send("a@example.com", "見積もりの件", true));
}
}
to=a@example.com subject=(件名なし) urgent=false
to=a@example.com subject=見積もりの件 urgent=true
Kotlin では、同じことが関数1つで書けます。次の Overloads.kt には、同じ内容の関数を2つ用意しました。違いは @JvmOverloads の有無だけです。
package demo
fun plain(to: String, subject: String = "(件名なし)", urgent: Boolean = false) =
"to=$to subject=$subject urgent=$urgent"
@JvmOverloads
fun send(to: String, subject: String = "(件名なし)", urgent: Boolean = false) =
"to=$to subject=$subject urgent=$urgent"
fun main() {
println(send("a@example.com"))
println(send("a@example.com", urgent = true))
}
to=a@example.com subject=(件名なし) urgent=false
to=a@example.com subject=(件名なし) urgent=true
Kotlin から呼ぶぶんには、どちらも同じように引数を省略できます。違いが出るのは Java から呼ぶときです。コンパイルしたクラスを javap で見ると、Java から見えるメソッドがわかります。
public final class demo.OverloadsKt {
public static final java.lang.String plain(java.lang.String, java.lang.String, boolean);
public static java.lang.String plain$default(java.lang.String, java.lang.String, boolean, int, java.lang.Object);
public static final java.lang.String send(java.lang.String, java.lang.String, boolean);
public static java.lang.String send$default(java.lang.String, java.lang.String, boolean, int, java.lang.Object);
public static final void main();
public static final java.lang.String send(java.lang.String, java.lang.String);
public static final java.lang.String send(java.lang.String);
public static void main(java.lang.String[]);
}
plain は引数3つの形だけ、send は引数3つ・2つ・1つの形がそろっています。Java から呼んで確かめます。
import demo.OverloadsKt;
public class Caller {
public static void main(String[] args) {
System.out.println(OverloadsKt.send("a@example.com"));
System.out.println(OverloadsKt.send("a@example.com", "見積もりの件"));
System.out.println(OverloadsKt.send("a@example.com", "見積もりの件", true));
}
}
to=a@example.com subject=(件名なし) urgent=false
to=a@example.com subject=見積もりの件 urgent=false
to=a@example.com subject=見積もりの件 urgent=true
@JvmOverloads のない plain を Java から引数1つで呼ぶと、javac が次のエラーを出します。
CallerPlain.java:5: error: method plain in class OverloadsKt cannot be applied to given types;
System.out.println(OverloadsKt.plain("a@example.com"));
^
required: String,String,boolean
found: String
reason: actual and formal argument lists differ in length
1 error
| 比べる点 | Java のオーバーロード | Kotlin のデフォルト引数 | Kotlin + @JvmOverloads |
|---|---|---|---|
| 書く関数の数 | 引数の組み合わせの数だけ | 1つ | 1つ |
| Kotlin から引数を省略 | ― | できる | できる |
| Java から引数を省略 | できる | できない(全引数が必要) | できる(後ろから順に省いた形) |
| 途中の引数だけ省略 | その形を用意すればできる | 名前付き引数でできる | Java からはできない |
@JvmOverloads が作るのは「既定値のある引数を後ろから順に省いた形」です。上の例では (to, subject, urgent)・(to, subject)・(to) の3つで、(to, urgent) の形は作られません。Java からも呼ばれる Kotlin のコードで、引数を省略して呼ばせたいときに付けます。
エラー文から引く早見表
この記事で再現したコンパイルエラーを、メッセージから逆引きできるようにまとめます。
| エラー文(Kotlin 2.4.20) | 原因 | 直し方 |
|---|---|---|
| return type mismatch: expected 'Unit', actual 'Int'. | ブロック本体で戻り値の型を書いていない | ): Int { のように型を書く |
| type checking has run into a recursive problem. | 単一式の再帰関数で型を省いた | 戻り値の型を書く |
| no value passed for parameter 'message'. | 既定値のない引数を渡していない | 引数を渡す、または名前付きで渡す |
| mixing named and positional arguments is not allowed unless the order of the arguments matches the order of the parameters. | 名前付き引数の後ろに、位置のずれた引数がある | 飛ばした後ろはすべて名前を付ける |
| argument type mismatch: actual type is 'IntArray', but 'Int' was expected. | vararg に配列をそのまま渡した | *arr と書く |
argument type mismatch: actual type is 'List<String>', but 'Array<out String>' was expected. |
List に * を付けた |
*list.toTypedArray() と書く |
| no value passed for parameter 'sep'. | vararg の後ろの引数を位置で渡した | sep = "-" と名前を付ける |
| 'val' cannot be reassigned. | 関数の引数に代入した | 別の変数を val や var で作る |
| an overriding function is not allowed to specify default values for its parameters. | override 側に既定値を書いた | 子クラス側の既定値を消す |
| 'infix' modifier is inapplicable to this function. | 引数が1つでない、またはメンバー・拡張関数でない | 条件を満たす形にするか infix を外す |
8行目の 'val' cannot be reassigned. は、次のコードで確認しました。Kotlin の引数は読み取り専用(val と同じ扱い)なので、計算結果は新しい変数に入れます。
// E7.kt
fun addTax(price: Int) {
price = price * 110 / 100
println(price)
}
E7.kt:3:5: error: 'val' cannot be reassigned.
price = price * 110 / 100
^^^^^
よくある質問
Q. 戻り値の型は、書くのと省くのとどちらがよいですか? A. ブロック本体で値を返す関数は必ず書きます。単一式関数は省けますが、再帰関数と、ほかのモジュールに公開する関数は書いておくのがおすすめです。本体を見ればすぐわかる短い非公開の関数は、省いても読みやすさは変わりません。
Q. デフォルト引数とオーバーロードは、どう使い分けますか? A. 「同じ処理で、一部の値に既定値があるだけ」ならデフォルト引数が向いています。引数の型や処理の内容そのものが違うとき(文字列で受ける形とファイルで受ける形など)は、Kotlin でもオーバーロードを使います。
Q. vararg と List を受け取る引数は、どちらにすればよいですか?
A. 呼び出す側が値をその場で並べて書くことが多いなら vararg、すでにコレクションを持っていることが多いなら List が呼びやすくなります。vararg に List を渡すときは *list.toTypedArray() と変換が入る点が判断材料になります。
Q. 関数の引数を関数の中で書き換えられますか?
A. 引数そのものには代入できません('val' cannot be reassigned.)。val taxed = price * 110 / 100 のように新しい変数を作ります。
まとめ
- 基本形は fun 関数名(引数: 型): 戻り値の型 { ... }。 式1つなら
=でつないで、波かっこ・return・戻り値の型を省ける - ブロック本体で戻り値の型を省くと Unit。 値を返すなら型と return を書く。
=と{ }を両方書くとラムダを返す関数になる - デフォルト引数は省略されたときだけ式が評価される。 必ず渡す引数を前、既定値のある引数を後ろに並べると呼びやすい
- 名前付き引数は、引数を飛ばしたらそこから後ろすべてに名前を付ける。 位置が宣言どおりなら混在できる
- vararg に配列を渡すときは
*。 List はtoTypedArray()で配列に変換する。vararg の後ろの引数は名前付きで渡す - Nothing は戻らない関数、ローカル関数は外に見せない処理、infix は引数1つのメンバー・拡張関数、tailrec は末尾再帰のループ化
- Java から引数を省略して呼ばせたいときは @JvmOverloads。 後ろから順に省いた形のメソッドが作られる
参考・出典
- Kotlin 公式ドキュメント「Functions」 https://kotlinlang.org/docs/functions.html
- Kotlin 公式ドキュメント「Calling Kotlin from Java」(@JvmOverloads) https://kotlinlang.org/docs/java-to-kotlin-interop.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin関数の書き方 fun・デフォルト引数・名前付き引数・vararg」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。