[Kotlin]函數

一、 概述

函數:也就是子程序。
高階函數:在數學和計算機科學中,高階函數是至少滿足下列一個條件的函數:接受一個或多個函數作為輸入,輸出一個函數
在Java中,不支持高階函數。也就是說函數不能作為參數,也不能作為返回值。
站在Java的基礎上,Kotlin開始支持高階函數。也就是說,函數在Kotlin中成為了一級公民。

二、 函數定義

2.1函數聲明

  • Kotlin 中的函數使用 fun 關鍵字聲明。
    函數的基本組成部分包括:名稱、參數、返回值和函數體,定義形式為:
fun methodName(param: ParamType): ReturnType {
  ...
}

  • 函數名參數列表作為函數的唯一的標識符

2.2、參數

  • 函數參數使用 Pascal 表示法定義,即 name: type。每個參數必須有顯式類型,多個參數之間使用逗號分隔.
fun methodName(number: Int, exponent: String) {
  ...
}

2.3默認參數

函數參數可以指定默認值, 當參數省略時, 就會使用默認值. 與其他語言相比, 這種功能使得我們可以減少大 量的重載(overload)函數定義.

//在參數類型之后, 添加 = 和默認值.
fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size) {
  ...
}
  • override方法與基類型方法使用相同的默認值。override時必須省略默認參數值:
open class A {
    open fun foo(i: Int = 10) { …… }
}

class B : A() {
    override fun foo(i: Int) { …… }  // 不能有默認值
}
  • 如果一個默認參數在一個無默認值的參數之前,那么該默認值只能通過使用命名參數調用該函數來使用:
fun foo(bar: Int = 0, baz: Int) { /* …… */ }

foo(baz = 1) // 使用默認值 bar = 0

2.4命名參數

調用函數時, 可以通過參數名來指定參數. 當函數參數很多, 或者存在默認參數時, 指定參數名可增加可讀性.

  • 通常與默認參數配合使用

例如:

fun login(name: String, no: Int = 1001, sex: Int = 0) {
    println("name: $name, no:$no, sex: $sex")
}

當采用默認方式時,我們可以這樣調用(無默認值參數位于參數列表的第一個時)

login("Aaron")

其實際上相當于

login("Aaron", 101, 0)

人如果我們不需要指定所有的參數, 只是修改部分默認的參數值,我們可以這樣:

register(name = "wang", no = 1003)

2.5不定數量參數

如果在函數被調用以前,函數的參數(通常是參數中的最后一個)個數不能夠確定,可以采用不定量參數方式:用 vararg 修飾符標記參數。
比如在創建List時,創建前并不知道預添加至List中多少數據。

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts 是一個 Array
        result.add(t)
    return result
}

調用時, 可以向這個函數傳遞不定數量的參數:

val list = asList(1, 2, 3)

  • 只有一個參數可以標注為 vararg

與Java不同的是,在Kotlin中標記為vararg的參數不一定是最后一個。如果標記為vararg的參數不是最后一個,那么vararg參數之后的其他參數, 可以使用命名參數來傳遞參數值, 或者, 如果參數類型是函數, 可以在括號之外傳遞一個 Lambda表達式.

fun main(args: Array<String>) {

    fruit("apple", "banana", address = "Minhang")

}

fun fruit(vararg fruits: String, address: String) {
    for (fruit in fruits) {
        println("fruit:$fruit, from address: $addr")
    }
}

// Log
fruit:apple, from address: Minhang
fruit:banana, from address: Minhang

  • 我們調用vararg函數時,我們可以一個接一個地傳參,例如 asList(1, 2, 3),或者,如果我們已經有一個數組并希望將其內容傳給該函數,我們使用伸展spread操作符(在數組前面加 *):
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

2.6 返回值Unit

如果一個函數不返回任何有意義的結果值,那么它的返回類型為Unit .Unit 類型只有唯一的一個值Unit,在函數中,不需要明確地返回這個值。

  • 返回值為Unit的函數,Unit可以省略。
fun login(name: String, no: Int = 1001, sex: Int = 0): Unit {
    println("name: $name, no:$no, sex: $sex")
}

上例中的代碼等價于:

fun login(name: String, no: Int = 1001, sex: Int = 0) {
    println("name: $name, no:$no, sex: $sex")
}

2.7 明確指定返回值類型

如果一個函數體由多行語句組成的代碼段,那么必須明確指定返回值類型,除非函數的的返回值為Unit。

2.8 單表達式函數

如果一個函數的函數體只有一個表達式,函數體可以直接寫在 “=”之后,也就是這樣:

fun double(x: Int): Int = x * 2

如果編譯器可以推斷出函數的返回值類型, 那么返回值的類型定義是可省略:

fun double(x: Int) = x * 2

2.9 返回值和跳轉

Kotlin有三種結構型的跳轉表達式:

  • return.默認返回最近的閉包函數和匿名函數
  • break.終結最近的閉包循環
  • continue.進行下一步最近的閉包循環

Break 與 Continue 標簽
在 Kotlin 中任何表達式都可以用標簽(label)來標記。 標簽的格式為標識符后跟 @ 符號,例如:abc@fooBar@都是有效的標簽。要為一個表達式加標簽,我們只要在其前加標簽即可。

loop@ for (i in 1..100) {
    // ……
}

現在,我們可以用標簽限制 break 或者continue

loop@ for (i in 1..100) {
    for (j in 1..100) {
        if (……) break@loop
    }
}

標簽限制的 break 會跳轉到‘該標簽指定的循環’后面的執行點(也就是最外層for循環)。

標簽處返回
Kotlin 的函數可以被嵌套。 標簽限制的 return 允許我們從外層函數返回。 最重要的用例是返回一個lambda表達式。
例如:

fun foo() {
    ints.forEach {
        if (it == 0) return
        print(it)
    }
}

return-expression從他最近的閉包函數當中返回,也就是foo.如果我們需要返回到ints.forEach的lambda表達式,我們必須給它做出標記

 fun foo(ints:List<Int>) {
        ints.forEach lit@ {
            if (it == 0) return@lit
            print(it)
        }
    }

現在,它僅僅從ints.forEach的lambda表達式處返回。

  • 通常情況下,使用隱喻標簽更方便(標簽與被傳入的lambda表達式的函數具有相同的名稱)。
fun foo() {
    ints.forEach {
        if (it == 0) return@forEach
        print(it)
    }
}

匿名函數與lambda同理,不再贅述。

三、函數的調用

3.1 傳統用法

函數的調用使用傳統的方式:

val result = double(2)

調用類的成員函數時, 使用點號標記法(dot notation):

Sample().foo() // 創建一個 Sample 類的實例, 然后調用這個實例的 foo 函數

3.2 中綴標記法(Infix notation)

使用中綴標記法(infix notation)來調用函數, 但函數需要滿足以下條件:

① 是成員函數, 或者是擴展函數
② 只有單個參數
③ 使用 infix 關鍵字標記

class Person(var name: String, var age: Int) {
   // 使用infix 關鍵字標記,該函數可被中綴標記法法調用
     infix fun printName(addr: String) {
         println("addr: $addr, name: $name")
     }
}

fun main(args: Array) { 
   val person: Person = Person(“Jone”, 20)
   // 使用中綴標記法調用擴展函數
   person printName("AA-BB") // Log: addr: AA-BB, name: Jone

   // 上面的語句等價于
   person.printName("AA-BB") 
}

四、函數的范圍

在Kotlin中,函數不僅僅能夠被定義為top_level,即包下的函數,還可以被定義為局部函數、成員函數以及擴展函數。函數的定義方式不同,其作用域也不盡相同。當然了,函數的作用域還與修飾符相關,具體可參考 Kotlin-可見性修飾符.

4.1 局部函數

所謂的局部函數,就是定義在函數體的函數。

fun function(input: String) {
    val param:Int = 101
    fun finctionInternal(Inputinternal:Int) {
        ...
    } 
...
    finctionInternal(param)
}

  • 局部函數可以訪問外部函數中的局部變量(也就是, 閉包),具體見上例。

4.2 成員函數

成員函數是指定義在類或對象之內的函數。

class Sample() {
    fun foo() { print("Foo") }
}

對成員函數的調用使用點號標記法。

Sample().foo() // 創建 Sample 類的實例, 并調用 foo 函數

4.3 擴展函數

擴展函數數是指在一個類上增加一種新的行為,甚至我們沒有這個類代碼的訪問權限。

  • 聲明一個擴展函數,我們需要用一個接收者類型也就是被擴展的類型來作為他的前綴。

/***
 * 點擊事件的View擴展
 * @param block: (T) -> Unit 函數
 * @return Unit
 */
fun <T : View> T.click(block: (T) -> Unit) = setOnClickListener {

    if (clickEnable()) {
        block(it as T)
    }
}
  • 擴展是靜態解析的
  • 如果一個類定義有一個成員函數和一個擴展函數,而這兩個函數又有相同的接收者類型、相同的名字并且都適用給定的參數,這種情況總是取成員函數。
class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

如果我們調用 C 類型 c的 c.foo(),它將輸出“member”,而不是“extension”。

可空接收者
可以為可空的接收者類型定義擴展。這樣的擴展可以在對象變量上調用, 即使其值為 null,并且可以在函數體內檢測 this == null,這能讓你在沒有檢測 null 的時候調用 Kotlin 中的toString():檢測發生在擴展函數的內部。

fun Any?.toString(): String {
    if (this == null) return "null"
    // 空檢測之后,“this”會自動轉換為非空類型,所以下面的 toString()
    // 解析為 Any 類的成員函數
    return toString()
}

具體例子可參考 :
1.[Kotlin]利用擴展函數優雅的實現“防止重復點擊”.
2.使用Kotlin高效地開發Android App(一).

4.4 尾遞歸函數

Kotlin支持一種稱為尾遞歸(tail recursion)的函數式編程方式.這種方式是基于函數表達式和遞歸函數,來實現某些基本循環的的算法,采用這種方式可以有效的避免棧溢出的危險。

當函數被關鍵字tailrec修飾,同時滿足尾遞歸(tail recursion)的函數式編程方式的形式時,編譯器就會對代碼進行優化, 消除函數的遞歸調用, 產生一段基于循環實現的, 快速而且高效的代碼。

tailrec fun plus(start: Int, end: Int, result: Int): Int = if (start >= end) result else plus(start+1, end, start + result) 

// Test
fun main(args: Array<String>) {

    println(plus(0, 10, 0)) // 打印結果 45
}

上面的代碼計算了從start到end之間的所有數的和,并將和與初始值相加后返回。編譯器優化產生的代碼等價于下面這種傳統方式編寫的代碼:

fun plus(start: Int, end: Int, result: Int): Int {
    var res = result
    var sta = start

    while (sta < end) {
        res += sta
        sta++
    }

    return res
}

注:

  1. 要符合 tailrec 修飾符的要求, 函數必須在它執行的所有操作的最后一步, 遞歸調用它自身
  2. 如果遞歸調用后還有其他邏輯代碼,不能使用尾遞歸
  3. 尾遞歸不能用在try/catch/finally 結構內
  4. 尾遞歸目前只能用在JVM環境內

五、高階函數

所謂的高階函數,是一種特殊的函數, 它接受函數作為參數, 或者返回一個函數.

fun test(a: Int, b: Int, sumSom: (Int, Int, Int) -> Int): Int {
    if (a > b) {
        return sumSom(0, a, 0)
    } else {
        return sumSom(0, b, 0)
    }
}

tailrec fun sumSom(start: Int, end: Int, result: Int): Int {
    var res = result
    var sta = start

    while (sta <= end) {
        res += sta
        sta++
    }

    return res
}

// 測試類
fun main(args: Array<String>) {
    println(test(10, 9, ::sumSom)) // Log:55
}

從上述代碼,在函數test中,sumSom參數是一個函數類型:(Int, Int, Int) -> Int,其是一個函數,接受3個Int參數,返回值是一個Int類型的值。在test中,對傳入的參數a,b進行判斷,然后執行sumSom()函數并將執行結果返回。

5.1 函數類型(Function Type)

對于接受另一個函數作為自己參數的函數, 我們必須針對這個參數指定一個函數類型. 比如, 前面提到的test函數, 它的定義如下:

fun test(a: Int, b: Int, sumSom: (Int, Int, Int) -> Int): Int {
    if (a > b) {
        return sumSom(0, a, 0)
    } else {
        return sumSom(0, b, 0)
    }
}

參數sumSom的類型是(Int, Int, Int) -> Int,也就是說,它是一個函數,接受三個Int類型參數,并且返回一個Int。

5.2 Lambda表達式與匿名函數

Lambda 表達式, 或者匿名函數, 是一種”函數字面值(function literal)”, 也就是, 一個沒有聲明的函數, 但是立即作為表達式傳遞出去.

max(strings, { a, b -> a.length() < b.length() })

函數 max 是一個高階函數, 也就是說, 它接受一個函數值作為第二個參數. 第二個參數是一個表達式, 本身又是另一個函數, 也就是說, 它是一個函數字面量. 作為函數, 它等價于:

fun compare(a: String, b: String): Boolean = a.length() < b.length() 

Lambda表達式

Lambda 表達式的完整語法形式, 也就是, 函數類型的字面值, 如下:

val sum = { x: Int, y: Int -> x + y }

① Lambda 表達式用大括號括起,
② 它的參數(如果存在的話)定義在 -> 之前 (參數類型可以省略),
③ 函數體定義在 -> 之后 (如果存在 -> 的話).

  • 如果Lambda 表達式只有唯一一個參數,在Kolin中可以自行判斷出Lambda表達式的參數定義,此時允許我們省略唯一一個參數的定義, 并且會為我們隱含地定義這個參數, 使用的參數名為 it:
ints.filter { it > 0 } // 這個函數字面值的類型是 '(it: Int) -> Boolean'

  • 如果一個函數接受另一個函數作為它的最后一個參數, 那么Lambda表達式作為參數時, 可以寫在圓括號之外.

可參考View.OnClickListener在Kotlin中的進化

匿名函數

匿名函數看起來與通常的函數聲明很類似, 區別在于省略了函數名,函數體可以是一個表達式(如上例), 也可以是多條語句組成的代碼段:

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

參數和返回值類型的聲明與通常的函數一樣, 但如果參數類型可以通過上下文推斷得到, 那么類型聲明可以省略:

ints.filter(fun(item) = item > 0)

對于匿名函數, 返回值類型的自動推斷方式與通常的函數一樣: 如果函數體是一個表達式, 那么返回值類型
可以自動推斷得到, 如果函數體是多條語句組成的代碼段, 則返回值類型必須明確指定(否則被認為是
Unit ).

5.3 內聯函數

高階函數會帶來一些運行時的效率損失:每一個函數都是一個對象,并且會捕獲一個閉包( 即那些在函數體內會訪問到的變量)。 內存分配(對于函數對象和類)和虛擬調用會引入運行時間開銷。
在許多情況下通過內聯化 lambda 表達式可以消除這類的開銷。
lock() 函數為例:

lock(l) { foo() }

編譯器沒有為參數創建一個函數對象并生成一個調用。取而代之,編譯器可以生成以下代碼:

l.lock()
try {
    foo()
}
finally {
    l.unlock()
}

這個才是我們想要的。為了讓編譯器這么做,我們需要使用inline修飾符標記 lock() 函數:

inline fun <T> lock(lock: Lock, body: () -> T): T {
    // ……
}

  • 內聯可能導致生成的代碼增加;不過如果我們使用得當(即避免內聯過大函數),性能上會有所提升,尤其是在循環中的“超多態(megamorphic)”調用處。

  • 通過noinline禁用內聯

如果你只想被(作為參數)傳給一個內聯函數的 lamda 表達式中只有一些被內聯,你可以用 noinline 修飾符標記一些函數參數:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
    // ……
}

參考:
https://hltj.gitbooks.io/kotlin-reference-chinese/content/txt/functions-and-lambdas.html
https://blog.csdn.net/io_field/article/details/53365834

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容