匿名函數

在前面的章節我們認識了函數,不過我們寫的函數都有名字。接下來我們會看到一種“沒有名字”的函數,我們把它稱之為匿名函數或是lambda函數。我們先來看看一個lambda函數的簡單例子

fun main() {
    println({ "Hello Kotlin!" }())
}

lambda類型

lambda函數跟一般的函數一樣,有傳入0-多個東西然後傳出1個東西的功能,那我們來看看lambda函數的型態要怎麼表示:

fun main() {
    val helloFunction: () -> String = { "Hello Kotlin!" }
    println(helloFunction())
}

在這個範例裡面,我們把前面的lambda函數拆開來,並且設定給一個變數。我們知道變數只能裝符合型態的東西,所以我們必須了解lambda函數的型態是什麼。

() -> String

lambda函數的型態我們會寫成這樣,從左到右依序是“(參數型態們)”、“ -> ”、“回傳型態”。用這樣的定義我們來看看上面的範例“() -> String”,指的就是這個lambda函數沒有參數,最後會回傳一個字串的意思。

隱式返回

在上面的lambda函數裡面有沒有發現雖然我們說是函數,但是最後並沒有return呢?其實在大多數情況下lambda函數並不需要使用return來回傳東西,lambda函數會回傳“最後一句話的結果”。

而為什麼return寫在這裡反而會錯呢?是因為這樣編譯器沒辦法判斷我們要返回的是哪一層的函數。所以假如我們真的有必要寫道return的話,我們可以幫lambda函數上“標籤”:

fun main() {
    val helloFunction: () -> String = hello@{ return@hello "Hello Kotlin!" }
    println(helloFunction())
}

利用標籤來讓編譯器知道我們要返回的是什麼函數,這樣編譯器就不會搞混了,而我們在後面會討論到有時候lambda函數會有“預設標籤”這一個特性。

參數

lambda函數既然也是函數,那就會有要傳入東西的需求,接下來我們就來看看下面的範例:

ex. 6-2-1
fun main() {
    val helloFunction: (String) -> String = { name ->
        "Hello $name!"
    }
    println(helloFunction("Kotlin"))
}

lambda函數的參數我們會寫在大括號的後面,然後再接一個箭頭(->),而且傳入引數的方式也跟我們在呼叫一般函數的方式一樣。同樣的如果我們想要定義多個參數的話,也是一樣的方法:

fun main() {
    val helloFunction: (String, Int) -> String = { name, no ->
        "Hello $name! You are No.$no."
    }
    println(helloFunction("小黑", 7))
}

我們傳入了多個參數型態,這些我們全部寫進“( )”裡面並用“,”隔開,參數名字也是全部寫在箭頭(->)前面,並用“,”隔開

it關鍵字

lambda函數有一個特別的關鍵字it,他只會出現在只有一個參數的lambda函數裡面。我們使用ex. 6-2-1的程式碼來示範

fun main() {
    val helloFunction: (String) -> String = { 
        "Hello $it!"
    }
    println(helloFunction("Kotlin"))
}

我們看到我們省略了明確的參數名字還有箭頭(->),並且使用“it”來代替參數,這個是在只有一個一個參數的lambda函數裡面特別可以使用的。

把lambda函數當成參數

我們也可以把lambda函數當成另一個函數的參數,這樣可以增加方法的彈性:

fun main() {
    printFormattedMessage("Kotlin", { "Hello $it!" })
    printFormattedMessage("Android", { "Hi! $it!" })
    printFormattedMessage("クロさん", { "こんにちは! $it!" })
}

fun printFormattedMessage(name: String, formatFunction: (String) -> String) {
    println(formatFunction(name))
}

簡略語法

如果lambda函數是所有參數的最後一個的話,我們可以把lambda寫在“( )”後面:

fun main() {
    printFormattedMessage("Kotlin") { "Hello $it!" }
    printFormattedMessage("Android") { "Hi! $it!" }
    printFormattedMessage("クロさん") { "こんにちは! $it!" }
}

fun printFormattedMessage(name: String, formatFunction: (String) -> String) {
    println(formatFunction(name))
}

因為有這樣的簡略語法,通常我們在定義函數,只有一個參數是lambda函數的時候,我們就會把lambda函數放在參數的最後一個,並在呼叫的時候使用簡略語法。這樣字寫起來比較簡單易讀。

函數內聯

接下來我們來介紹內聯“inline”關鍵字,在Kotlin裡面我們會大量的使用lambda函數,這是程式語言提供的優勢,可是當程式碼編譯在JVM上的時候每一個lambda函數都會變成物件實例,這樣在做大量的資料處理的時候會對我們的效能產生很大的負擔。為了避免這樣的狀況我們可以使用“inline”關鍵字,使用inline關鍵字後,呼叫函數的時候並不會產生lambda函數的物件實例,而是會把函數的內容“貼上”一份在呼叫函數的地方,這樣可以增加效能。

不過內聯函數並不是萬用解,在某些時候並不能使用內聯函數,而通常這種時候編譯器也會提醒我們,比如:遞迴函數

函數參照

lambda函數很方便,可是如果我們想要使用現有的函數當作引數傳入的話要怎麼辦呢?

fun main() {
    printFormattedMessage("Kotlin", ::helloMessage)
}

fun printFormattedMessage(name: String, formatFunction: (String) -> String) {
    println(formatFunction(name))
}

fun helloMessage(name: String): String {
    return "Hello $name!";
}

這邊我們看到了新的運算子“::”,他可以把一個有名字的函數轉換成一個引數,讓我們可以把函數傳入函數裡面,只要是函數的型態跟要傳入的lambda函數一樣的時候都可以使用函數參照來傳入具名的函數。

使用lambda函數作為回傳值

lambda函數可以被當作參數,當然也可以當作回傳值,而且當我們看到下面的範例,我們可以發現lambda函數一些有趣的特性:

fun main() {
    val helloMessageFunction = configureHelloMessageFunction()
    println(helloMessageFunction("小黑"))
    println(helloMessageFunction("小固"))
    println(helloMessageFunction("白助"))
}

fun configureHelloMessageFunction(): (String) -> String {
    var number = 1
    return { "Hello $it! You're number is ${number++}." }
}

我們發現在執行的時候,回傳的lambda函數每被呼叫一次,同學的座號就會被加1。為什麼會這樣呢?其實當我們在使用lambda函數的時候,我們可以把每一次“實體化的lambda函數”當成是一個自己的小世界,在裡面取用的變數他的結果會被保留下來,所以就會造成這樣的結果。

typealias

寫了這麼多lambda函數,有時候我們會覺得要寫這種“(......) -> ......”的型態我們會覺得很醜或是有時候我們看著看著就搞錯這個lambda函數是什麼作用。這個時候我們就可以使用“typealias”關鍵字來幫lambda函數命名,以上一個範例來說,我們可以改成這樣:

fun main() {
    val helloMessageFunction: HelloMessageFactory = configureHelloMessageFunction()
    println(helloMessageFunction("小黑"))
    println(helloMessageFunction("小固"))
    println(helloMessageFunction("白助"))
}

typealias HelloMessageFactory = (String) -> String

fun configureHelloMessageFunction(): HelloMessageFactory {
    var number = 1
    return { "Hello $it! You're number is ${number++}." }
}

練習

  1. 定義一個函數可以自行決定學號的輸出格式並在main函數裡呼叫兩次,分別讓他輸出

  • “TIP101-__”

  • “400420__S”

fun printStudentId(number: Int, translation: (String) -> String) {
    // 把 數字 1 -> "01"
    val formattedNumber = String.format("%02d", number)
    // 程式碼
}

Last updated