《瘋狂Kotlin講義》讀書筆記6——函式和Lambda表示式

鳳青發表於2021-04-28

函式和Lambda表示式

Kotlin融合了程式導向語言和麵向物件語言的特徵,相比於Java,它增加了對函數語言程式設計的支援,支援定義函式、呼叫函式。相比於C語言,Kotlin支援區域性函式(Lambda表示式的基礎)。

6.1 函式入門

6.1.1 定義和呼叫函式

定義函式的語法格式如下:

fun 函式名 (形參列表) [: 返回值型別] {
	//函式體
}
// 函式的宣告必須使用fun關鍵字

// 形參列表   “形參名: 引數型別”

// 舉例:
fun max(x: Int, y: Int): Int {
    val res = if(x > y) x else y
    return res
}

6.1.2 函式返回值和Unit

如果希望一個函式沒有返回值,可以通過以下兩種方式實現:

  1. 省略“: 返回值型別”部分
  2. 使用“: Unit”指定返回Unit -> 代表沒有返回值
fun func1(str: String) {
	println(str)
}

fun func2(str: String): Unit {
    println(str)
}

6.1.3 遞迴函式

在函式體內呼叫該函式本身。

......

6.1.4 單表示式函式

如果函式只是返回單個表示式,那麼可以省略花括號並在函式名後用等號指定函式體。這種形式的函式被稱為單表示式函式。

fun area1(x: Int, y: Int): Int {
    return x*y;
}

fun area2(x: Int, y: Int): Int = x*y

//函式area1和函式area2等效,其中area2是單表示式函式

6.2 函式的形參

相比於Java,Kotlin形參的功能更加豐富且靈活。

6.2.1 命名引數

Kotlin中引數名是有意義的。

Kotlin除了可以按照傳統的方式——根據引數的位置(順序)來傳入引數之外,還可以使用命名引數來傳參(此時可以不按順序傳參)。

還可以以上二者混合使用,但此時要求位置引數必須位於命名引數之前。

fun area(width: Double, height: Double): Double {
    var a = width * height
    println("width=${width}, height=${height}, area=${a}")
    return a
}

var res: Double

//傳統的傳參方式——位置引數,根據形參位置傳參
res = area(2.0, 5.0)  

//Kotlin中支援的傳參方式——命名引數,根據形參名傳參
res = area(heigth = 5.0, width = 2.0)

//二者的混合使用
res = area(2.0, height = 5.0)

6.2.2 形參預設值

在Kotlin中,使用者可以在定義函式時為一個或多個形參指定預設值——這樣在呼叫函式時,就可以省略該形參值的傳入,而使用形參預設值。語法格式如下:

形參名: 形參型別 = 預設值

fun sayHi(name: String = "Seraph", msg: String = "hello !") {
    println("${name}, ${msg}")
}

sayHi()  //全部使用預設引數,輸出:Seraph, hello !
sayHi("Jack", "see you !")  //全部使用位置引數
sayHi(name = "Leo", msg = "bye~") //全部使用命名引數
sayHi(msg = "bye~")  //預設引數 + 命名引數
sayHi("Ben")  // 位置引數 + 預設引數, 輸出:Ben, hello !

通常建議將帶預設值的引數定義在形參列表的最後。

6.2.3 尾遞迴函式

當函式將自身作為其執行體的最後一行程式碼,且遞迴呼叫後沒有更多的程式碼時,可以使用尾遞迴語法。

尾遞迴不能在try、catch、finally塊中使用。

尾遞迴函式需要使用tailrec修飾。

//定義計算階乘的函式
fun fact(n: Int): Int {
    if(n == 1) {
        return 1
    } else {
        return n * fact(n-1)
    }
}

//使用尾遞迴語法改寫上述函式
tailrec fun factRec(n: Int, total: Int = 1): Int = if(n == 1) total else factRec(n-1, total * n)

編譯器會將尾遞迴優化成一個快速且高效的基於迴圈的版本,這樣可以減少對記憶體的消耗。

6.2.4 個數可變的形參

Kotlin中,我們可以通過在形參名前新增 vararg修飾符,以定義個數可變的形參,從而為函式指定數量不定的形參。

fun test(a: Int, vararg books: String) {
    //books被當做陣列處理
    for(book in books) {
        println(book)
    }
}

Kotlin允許個數可變的形參位於引數列表中的任意位置,但是要求一個函式只能帶一個形參。

6.3 函式過載

一個kt檔案中包含了兩個或兩個以上函式名相同、形參列表不同的函式,被稱為函式的過載。

與Java類似的是,Kotlin中函式的過載也只能通過形參列表進行區分,形參個數不同、形參型別不同都可以算作函式的過載。但是形參名不同、返回值型別不同或修飾符不同,都不能算函式的過載。

6.4 區域性函式

Kotlin支援在函式體內定義函式,這種定義在函式體內的函式別稱之為區域性函式。

預設情況下,區域性函式是對外部隱藏的,只在其封閉函式內有效。其封閉函式也可以返回區域性函式,以便程式在其他作用域中使用區域性函式。

fun A(type: String) {
    fun B(name: String) {
        println("name=${name}")
    }
}

6.5 高階函式

Kotlin中函式也是一等公民,函式本身也有自己的型別——函式型別。函式型別既可以用於定義變數,也可以用於形參、返回值。

6.5.1 使用函式型別

函式型別由:函式形參列表、-> 、返回值型別 這三者組成。

fun foo(a: Int, name: String) -> String {
    ......
}
// 函式foo的函式型別為:(Int, String) -> String

fun bar() {
    ......
}
// 函式bar的函式型別為:() 或 () -> Unit

使用函式型別定義變數的方法如下:

var myFun: (Int, Int) -> Int

fun pow(base: Int, exponent: Int): Int {
    var res = 1
    for(i in 1 .. exponent) {
        res *= base
    }
    return res
}

fun area(width: Int, height: Int): Int = width * height

myFun = ::pow  //將pow函式賦值給myFun
println(myFun(2, 3))  //輸出:8

myFun = ::pow  //將area函式賦給myFun
println(myFun(2, 3))  //輸出:6

當直接訪問一個函式的函式引用,而不是呼叫該函式時,需要在函式名前新增兩個冒號,而且不能在函式名後新增圓括號——一旦新增圓括號就變成了呼叫函式而不是訪問函式引用。

6.5.2 使用函式型別作為形參

fun map(data: Int, fn: (Int) -> Int): Unit {
    ......
}

6.5.3 使用函式型別作為返回值型別

fun getMathFunc(type: String): (Int) -> Int {
    
    fun square(n: Int): Int = n * n
    
    fun cube(n: Int): Int = n * n * n
    
    fun factorial(n: Int): Int {
        var res = 1;
        for(index in 2 .. n) {
            res *= index
        }
        return res
    }
    
    when(type) {
        "square" -> return ::square
        "cube" -> return ::cube
        else -> return ::factorial
    }
}

6.6 區域性函式與Lambda表示式

Lambda表示式是現代程式語言爭相引入的語法,它是功能更靈活的程式碼塊,可以在程式中被傳遞和呼叫。

6.6.1 回顧區域性函式

6.6.2 使用Lambda表示式代替區域性函式

可以使用Lambda表示式來簡化區域性函式。

fun getMathFunc(type: String): (Int) -> Int {
    when(type) {
        "square" -> return {n: Int -> 
                           	 n * n
                           }
        "cube" -> return {n: Int -> 
                         	n * n * n
                         }
        else -> return { n: Int -> 
                        	var res = 1
                        	for(index in 2 .. n) {
                                res *= index
                            }
                        	res
                       }
    }
}

Lambda表示式和區域性函式區別如下:

  • Lambda表示式總是被大括號括著
  • 定義Lambda表示式不需要fun關鍵字,無需指定函式名
  • 形參列表(如果有)在 -> 之前宣告,引數型別可以省略。
  • 函式體(Lambda表示式的執行體)放在 -> 之後
  • 函式的最後一個表示式自動被作為Lambda表示式的返回值,無需使用 return 關鍵字

6.6.3 Lambda表示式的脫離

作為引數傳入的Lambda表示式可以脫離函式獨立使用。

6.7 Lambda表示式

Lambda表示式語法如下:

{ (形參列表) -> 
	零到多條可執行語句	
}

6.7.1 呼叫Lambda表示式

Lambda表示式可以被賦值給變數或者直接呼叫。

// 定義一個Lambda表示式,並將它賦值給square
var square = { n: Int ->
    n * n
}
println(square(5))  //輸出:25

// 定義一個Lambda表示式,並在其後新增圓括號來呼叫該表示式,並將結果賦給res
var res = { base: Int, exponent: Int ->
    var result = 1
    for(i in 1 .. exponent) {
        result *= base
    }
    result
}(4, 3)
println(res)  //輸出64

6.7.2 利用上下文推斷型別

如果Kotlin根據Lambda表示式的上下文推斷出形參型別,那麼Lambda表示式就可以省略形參型別。

// 變數square的型別已經被宣告瞭,因此Kotlin可以推斷出Lambda表示式的形參型別
// 所以Lambda表示式可以省略形參型別
var square: (Int) -> Int = {n -> n * n}

var res = {a, b -> a + b}   //非法,因為Kotlin無法推斷形參a,b的資料型別

6.7.3 省略形參名

如果只有一個形參,那麼Kotlin允許省略Lambda表示式的形參名。當形參名被省略時,Lambda表示式用 it來代表形參。同時, -> 也不需要了。

//  省略形參名,用it代替
var square: (Int) -> Int = {it * it}

6.7.4 呼叫Lambda表示式的約定

Kotlin中約定:如果函式的最後一個形參是函式型別,且你打算傳入一個Lambda表示式作為相應的引數,那麼就允許在圓括號之外指定Lambda表示式。

var list = listOf("Java", "Kotlin", "Go")
var rt = list.dropWhile(){it.length > 3}   //dropWhile()方法的引數列表為: (T) -> Boolean
println(rt)  //輸出: [Go]

如果Lambda表示式是函式呼叫的唯一引數,則呼叫時函式的圓括號可以省略:

var rt = list.dropWhile{it.length > 3}

6.7.5 個數可變的引數和Lambda引數

前面有提到:個數可變的形參可以定義在引數列表中的任意位置,但是如果不將它放在最後,就只能用命名引數的形式為可變形參之後的其他形參傳值。所以建議將可變形參放在形參列表的最後。

那麼我們到底是應該將函式型別的引數放在形參列表的最後,還是將個數可變的引數放在形參列表最後呢?

答案:如果一個函式既包含個數可變的形參,也包含函式型別的形參,那麼應該將函式型別的形參放在最後。

fun <T> test(vararg name: String, transform: (String) -> T): List<T> {
    ......
}

6.8 匿名函式

Lambda表示式無法指定返回值型別,且在一些特殊場景下Kotlin也無法推斷出Lambda表示式的返回值型別。此時可以匿名函式來代替Lambda表示式。

只要將普通函式的函式名去掉就成了匿名函式。

var test = fun(x: Int, y: Int): Int {
    return x + y
}

6.9 捕獲上下文中的變數和常量

Lambda表示式、匿名函式、區域性函式都可訪問或修改其所在上下文中的變數和常量。

6.10 行內函數

由於函式的呼叫過程會產生一定的時間和空間上的開銷,為了避免這部分開銷(即避免產生函式的呼叫過程),我們可以考慮通過行內函數 的方式,將被呼叫的函式或表示式”嵌入“原來的執行流中。

// 通過inline關鍵字宣告行內函數
inline fun mFun(data: Int) {
    ......
}

相關文章