第一級オブジェクトとは
- 以下の性質をもつもの
- 変数に代入できたり
- 関数の引数として渡せたり
- 関数の戻り値にしたり
- 関数も第一級オブジェクトとして上記のような使い方ができる
→関数オブジェクト
関数オブジェクト
- 関数名の前に
::
をつけることで関数オブジェクトを取得できる - オブジェクトを変数に代入し、それに引数を渡すと実行できる
fun square(i: Int): Int = i * i
println(::square) // fun square(kotlin.Int): kotlin.Int
val funObj = ::square
println(funObj(5)) // 25
関数型
- 先ほど変数に代入した関数の型は以下の用になっている
val funObj: (Int) -> Int = ::square
- これはSwiftのクロージャの書き方と同じ
- 戻り値の書き方が
:
で区切ったり->
で区切ったり…。->
はwhen
でも使われているし…。- この悩みはあとで説明されてスッキリするのかな?
高階関数
- 関数を引数として受け取ったり、返り値として返す関数のこと
- 関数の抽象化が可能になる
- これはSwiftのクロージャと同じ使い方
// 文字列の中から特定の条件に当てはまる最初のインデックスを返す
fun first(str: String, predicate: (Char) -> Boolean): Int {
tailrec fun go(str: String, index: Int): Int {
return when {
str.isEmpty() -> -1
predicate(str.first()) -> index
else -> go(str.drop(1), index + 1)
}
}
return go(str, 0)
}
// 最初に出てくるKを探す
fun firstK(str: String): Int {
fun isK(c: Char): Boolean = c == 'K'
return first(str, ::isK)
}
// 最初に出てくる大文字を探す
fun firstUpperCase(str: String): Int {
fun isUpperCase(c: Char): Boolean = c.isUpperCase()
return first(str, ::isUpperCase)
}
ラムダ式
- おさらい
- 関数オブジェクトの生成は
::関数名
- 関数オブジェクトの生成は
- 関数オブジェクトを直接生成する方法がラムダ式
val square: (Int) -> Int = { i: Int ->
i * i
}
// 型推論
val square1 = { i: Int ->
i * i
}
val square2: (Int) -> Int = { i ->
i * i
}
- 変数が1つのときのみ暗黙の変数
it
を使用できる
val square2: (Int) -> Int = {
it * it
}
さっきの例でラムダ式を使ってみる
fun firstK(str: String): Int {
val isK: (Char) -> Boolean = { c: Char ->
c == 'K'
}
return first(str, isK)
}
// ラムダ式そのまま生成して渡す
fun firstK(str: String): Int {
return first(str, { it == 'K' })
}
// 引数の最後がラムダ式の場合はラムダ式を引数リストの外に出せる
// Swiftと同じ
fun firstK(str: String): Int = first(str) { it == 'K' }
クロージャ
- 外部のスコープにある変数にアクセスできる関数オブジェクトのこと
- ラムダ式
- 無名関数(後述)
- など
var sum = 0
ints.forEach {
sum += it
}
print(sum)
インライン関数
- 高階関数は強力な仕組みだが、一般的に呼び出しコストが高い傾向にある
- 関数オブジェクトの生成
- クロージャとして県境内の変数の補足
- →インライン関数
- 引数の関数オブジェクトがコンパイル時にインライン展開される関数
- 実行時にオブジェクトの生成などをする必要がなくなる
inline fun log(debug: Boolean = true, message: () -> String) {
if (debug) {
println(message())
}
}
fun main(args: Array<String>) {
log { "出力される" }
log(false) { "出力されない" }
}
上記をコンパイルすると
以下のようなコードと同じようなバイトコードが生成される
fun main(args: Array<String>) {
if (true) {
println("出力される")
}
if (false) {
println("出力されない")
}
}
Kotlinの標準ライブラリで使われている高階関数の多くはインライン関数になっている
非ローカルリターンとラベルへのリターン
- おさらい
- 通常の関数:return文が返り値
- ラムダ式:最後の式の評価結果が返り値
- ラムダ式内では外側の関数からのリターンが可能
→非ローカルリターン - ラムダ式はインライン展開されている必要がある
inline fun forEach(str: String, f: (Char) -> Unit) {
for (c in str) {
f(c)
}
}
fun containsDigit(str: String): Boolean {
forEach(str) {
if (it.isDigit()) {
return true
}
}
return false
}
- 外部の関数ではなく、自身から脱出したい
→ラベルへのリターン
fun containsDigit(str: String): Boolean {
var result = false
forEach(str) here@ {
if (it.isDigit()) {
result = true
return@here
}
}
return result
}
// リターン対象が推測できる場合に限り、ラベル名ではなく関数名を指定することができる
fun containsDigit(str: String): Boolean {
var result = false
forEach(str) {
if (it.isDigit()) {
result = true
return@forEach
}
}
return result
}
無名関数
- ラムダ式のように関数オブジェクトを直接得る方法の1つ
- ラムダ式との唯一の違いは、非ローカルリターンができない点
// ラムダ式
val square: (Int) -> Int = { it * it }
// 無名関数
val square2: (Int) -> Int = fun(i: Int): Int {
return i * i
}
// 無名関数 省略
val square3: (Int) -> Int = fun(i: Int) = i * i