最近、Kotlin+Spring BootでRESTful APIを書いてます。(とは言っても片手間でやっているので遅々として進みませんが)
コード書いていて「おやっ?」って思った表題の件について取り上げてみます。
お題とするコード
あんまり良い例が思いつかなったので、相当違和感あるコードかもしれませんがご勘弁ください。
以下の様なクラスのコレクションがあります。
data class Country(val name: String, val continent: String, val population: Long)
val counties = listOf(
Country("France", "Europe", 67_158_000),
Country("Germany", "Europe", 82_349_400),
Country("United Kingdom", "Europe", 65_648_000),
Country("Japan", "Asia", 126_672_000),
Country("India", "Asia", 1_324_171_354),
Country("China", "Asia", 1_403_500_365)
)
リストに指定した国名が含まれているかをチェックする関数を書きます。標準関数で事足りる話ですが、あえて冗長なコードを書きます。
fun isContainCountry(countries: List<Country>, name: String): Boolean {
var filtered = countries.filter { c ->
println(c.name)
c.name == name
}
println("(Country)$filtered")
return !filtered.isEmpty()
}
関数を呼び出してみると以下の様な出力になります。
>>> println("[RESULT]${isContainCountry (counties, "Japan")}")
France
Germany
United Kingdom
Japan
India
China
(Country)[Country(name=Japan, continent=Asia, population=126672000)]
[RESULT]true
>>> println("[RESULT]${isContainCountry (counties, "United States")}")
France
Germany
United Kingdom
Japan
India
China
(Country)[]
[RESULT]false
ここからが本題
大した話でもないのに前置きが長くなりましたが本題です。
前の関数から少しだけ変更します。
fun isContainCountryNG(countries: List<Country>, name: String): Boolean {
var filtered = countries.filter { c ->
println(c.name)
return c.name == name // returnをつける
}
println("(Country)$filtered")
return !filtered.isEmpty()
}
これを実行すると、
>>> println("[RESULT]${isContainCountryNG (counties, "Japan")}")
France
[RESULT]false
意図したような挙動になりません。
ラムダ内のreturnは、そのラムダを呼び出した関数からのリターンを意味します。(非ローカルリターン
と呼ぶそうです)
つまり、いきなりisContainCountryNG関数の処理が終わってしまうことになります。
このような挙動になる理由は、filter関数がインライン展開されるからです。
filterの定義を見てみると、inline
修飾子がついています。
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
インライン展開されたコードを思い浮かべれば、その中のreturnにより関数からリターンされるということがイメージできます。
C#のラムダだとこういうケースではreturnがないとコンパイルエラーだったように思います。ので、ついreturnを書いてしまうこともあるかもしれません。
以下の様に書くと、この非ローカルリターン
もしっくりくると思います。(そもそも標準関数を使えば良いという話はさておいて)
fun isContainCountryForEach(countries: List<Country>, name: String): Boolean {
countries.forEach { c ->
println(c.name)
if (c.name == name) return true
}
return false
}
実行結果は、
>>> println("[RESULT]${isContainCountryForEach (counties, "Japan")}")
France
Germany
United Kingdom
Japan
[RESULT]true
となります。
ローカルリターン
非ローカルリターン
に対してローカルリターン
というのが当然あります。
ローカルリターンにする方法は2つあります。
ラベル付きreturn
fun isContainCountryLR(countries: List<Country>, name: String): Boolean {
var found = false
countries.forEach { c ->
if (c.name == name) {
println("I found it!")
found = true
return@forEach // ラベル付きリターン
}
println(c.name) // returnするとこの行は実行されない
}
println("Exit...")
return found
}
無名関数
fun isContainCountryAF(countries: List<Country>, name: String): Boolean {
var found = false
countries.forEach ( fun(c) { // 無名関数
if (c.name == name) {
println("I found it!")
found = true
return
}
println(c.name) // returnするとこの行は実行されない
})
println("Exit...")
return found
}
どちらも出力結果は同じです。
>>> println("[RESULT]${isContainCountryLR (counties, "Japan")}")
France
Germany
United Kingdom
I found it!
India
China
Exit...
[RESULT]true