- Vixual - http://www.vixual.net/blog -

[Kotlin]研究 Kotlin 的函式宣告、函式型別、匿名函式、Lambda 與高階函式

Kotlin 的「函式」是所謂的「一級函式」,支援「高階函式」的用法,也可宣告「匿名函式」及「巢狀函式」,這些都是近幾年所興起的程式語言特色。

(原本這篇文章只是要整理一下 Kotlin 的函式用法而已,沒想到愈寫愈多!!)

名詞定義

先確認是否了解什麼是表達式? 什麼是敘述式?

Expression (表達式、表示式、運算式)

Statement (敘述式、語句)

由會產生「動作」的程式關鍵字及語法所組成的程式碼

其它名詞定義

Kotlin 函式種類

先看這張整理好的表格:

種類 語法 速記 函式主體 本質 傳回值
具名函式 A.敘述式 fun f():{} 可多行 函式 return
B.單一表達式 fun f()= 一行 函式 隱性傳回
匿名函式 A.仿敘述式 fun():{} 可多行 物件 return
B.單一表達式 fun()= 一行 物件 隱性傳回
C.Lambda {->} 可多行 物件 隱性傳回
函式物件 f:()->=匿名函式
~ 物件 ~

以下以簡單的 sum() 函式做為範例來看 Kotlin 各種函式的寫法。

具名函式

A. 敘述式:

fun sum (m:Int, n:Int): String {
    return "Result: ${m + n}"
}

B. 單一表達式:

fun sum (m:Int, n:Int): String = "Result: ${m + n}"

呼叫函式與傳遞引數:

println(sum(1, 2)) // Result: 3

匿名函式 (物件)

A. 仿敘述式:

fun(m:Int, n:Int): String {
    return "Result: ${m + n}"
}

B. 單一表達式:

fun(m:Int, n:Int): String = "Result: ${m + n}"

C. Lambda:

{ m:Int, n:Int ->
    "Result: ${m + n}"
}

可以看到 Lambda 具有所有匿名函式的優點。但是這些匿名函式有什麼屁用呢? 請接著看下去。

函式物件

在「一級函式」中,函式可以被當作「值」指派給另一個「變數」。Kotlin 的「函式物件」是由「匿名函式」轉化而來的,是將「匿名函式」指派給另一個變數,成為「函式物件」。

函式型別:

Kotlin 是一種「強型別」語言,每一種值都有它的「資料型別」,因此函式物件也有屬於自己的型別,稱之為「函式型別」。

例如,用「資料型別」來宣告一般變數的語法:

var 變數名稱: 資料型別
var 變數名稱: 資料型別 = 值

用「函式型別」來宣告函式物件:

var 函式名稱: 函式型別
var 函式名稱: 函式型別 = 匿名函式

「函式型別」的宣告比較複雜! 因為要識別整個匿名函式的「輸入型別」及「輸出型別」,所以寫做 (輸入型別)->輸出型別。若該匿名函式沒有輸入也沒有輸出則寫做 ()->Unit

並且,跟原本的變數宣告一樣,只要可以從「等號」的右邊推論出型別,那左邊的函式型別也可以省略不寫 (這裡的「省略不寫」只是程式設計師不用打字上去而已,實際上,編譯器會偷偷幫你把型別補上去)。

以下是將 sum() 範例用「函式型別」改寫成函式物件。

A. 仿敘述式:

var sum: (Int, Int) -> String = fun(m:Int, n:Int): String {
     return "Result: ${m + n}"
}
// 或是省略函式型別:
var sum = fun(m:Int, n:Int): String {
     return "Result: ${m + n}"
}

B. 單一表達式:

var sum: (Int, Int) -> String =
    fun(m:Int, n:Int): String = "Result: ${m + n}"
// 或是省略函式型別:
var sum =
    fun(m:Int, n:Int): String = "Result: ${m + n}"

C. Lambda:

var sum: (Int, Int) -> String = { m: Int, n: Int ->
    "Result: ${m + n}"
}
// 或是省略函式型別:
var sum = { m: Int, n: Int ->
    "Result: ${m + n}"
}

呼叫函式與傳遞引數:

函式物件要傳入引數必需用 invoke() 方法,例如:

println(sum.invoke(1, 2)) // Result: 3

但 Kotlin 用語法糖包裝函式物件,讓你用 sum(1, 2) 就可以直接傳入引數:

println(sum(1, 2)) // Result: 3

函式名稱衝突:

Kotlin 允許相同名稱的「具名函式」與「函式物件」同時存在,但具名函式的優先權高於函式物件。執行 functionName() 會呼叫「具名函式」,此時若要呼叫「函式物件」,必須用函式物件原本的方法 invoke():

functionName.invoke()

其它用法:

1. 若 Lambda 函式只有一個輸入參數,可用 it 取代:

var inc = {
    "Result: ${it + 1}"
}
println(inc(2)) // Result: 3

2. 巢狀 Lambda:

var sum: (Int, Int) -> (Int, Int) -> String = { m: Int, n: Int ->
    { x: Int, y: Int ->
        "Result: ${m + n + x + y}"
    }
}
// 或是省略函式型別:
var sum = { m: Int, n: Int ->
    { x: Int, y: Int ->
        "Result: ${m + n + x + y}"
    }
}
println(sum(1, 2)(3, 4)) // Result: 10

3. 可用 :: 將「具名函式」轉成「函式物件」:

fun oldSum(m:Int, n:Int): String {
    return "Result: ${m + n}"
}
println((::oldSum)(1, 2)) // Result: 3
var sum = ::oldSum
println(sum(1, 2)) // Result: 3

高階函式

Kotlin 支援所謂的「高階函式」,意思是函式可以作為「參數」傳入另一個函式,也可以被函式作為「傳回值」,而這些特性通常與「匿名函式」與「函式物件」相關。

範例 1

以下的範例是對 Kotlin 既有的 Int 整數型別「擴充」一個名為 check() 的函式,這個函式的功能是「讓你傳入另一個函式」去做你想要的檢查。這個範例可以讓你體會一下「高階函式」的用途,以及各種匿名函式的寫法。

// 函式宣告
fun Int.check(fx: (Int) -> Boolean): Boolean {
    return fx(this)
}

不專業的圖解:

// 使用匿名函式,判斷整數 7 是否大於 9
// 1. 仿陳述式
println(7.check(fun(n: Int): Boolean { return n > 9 })) // false

// 2. 使用表達式
println(7.check(fun(n: Int) = n > 9)) // false

// 3. 使用 Lambda
println(7.check({ n: Int -> n > 9 })) // false

Lambda 簡化:

當 Lambda 做為函式的「引數」時,還可以有一些簡化:

  1. 可用 it 取代唯一參數
  2. 若 Lambda 為最後一個引數,可提取至小括號的後面
  3. 若 Lamba 為唯一的引數,可省略小括號

因此上面的 Lambda 執行範例可以再簡化如下:

println(7.check({ it > 9 })) // false
println(7.check() { it > 9 }) // false
println(7.check { it > 9 }) // false

高階函式的好處:

使用高階函式可以讓你任意更換程式邏輯,例如:

println(7.check { it % 2 == 0 }) // 判斷整數 7 是否為偶數
println(12.check { it in 10..100 }) // 判斷整數 12 是否介於 10 ~ 100

範例 2

以下是帶多個參數的範例,最後一個參數為匿名函式。

// 函式宣告
fun Int.demo(text: String, fx: (Int, String) -> Unit): Unit {
    return fx(this, text)
}

不專業的圖解:

// 用 Lambda 傳入的執行結果
5.demo("cars", { n, text -> println("The number of $text is $n") }) // The number of cars is 5
9.demo("cars") { n, text -> println("The number of $text is $n") } // The number of cars is 9

不專業的結論