Kotlin學習筆記6——普通函數

前言

上一篇,我們學習了Kotlin中的返回和跳轉,今天繼續來學習Kotlin中的函數。由於Kotlin中支持高階函數語法,所以函數我們分爲三篇來學習,今天是第一篇:普通函數。

普通函數

先看下平常使用的普通函數

函數聲明

Kotlin 中的函數使用 fun 關鍵字聲明:

fun double(x: Int): Int {
    return 2 * x
}

函數用法

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

val result = double(2)

調用成員函數使用點表示法:

Stream().read() // 創建類 Stream 實例並調用 read()
//或者
Stream()::read 

參數

函數參數使用 Pascal 表示法定義,即 name: type。參數用逗號隔開。每個參數必須有顯式類型,函數體前面的是返回類型

fun powerOf(number: Int, exponent: Int): Int { /*函數體*/ }

默認參數

函數參數可以有默認值,當省略相應的參數時使用默認值。與其他語言相比,這可以減少重載數量:

fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size) { /*……*/ }

默認值通過類型後面的 = 及給出的值來定義。

覆蓋方法總是使用與基類型方法相同的默認參數值。 當覆蓋一個帶有默認參數值的方法時,必須從簽名中省略默認參數值:

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

如果在默認參數之後的最後一個參數是 lambda 表達式,那麼它既可以作爲具名參數在括號內傳入,也可以在括號外傳入:

fun foo(bar: Int = 0, baz: Int = 1, qux: () -> Unit) { /*……*/ }

foo(1) { println("hello") }     // bar = 1,使用默認值 baz = 1
foo(qux = { println("hello") }) // 使用兩個默認值 bar = 0 與 baz = 1
foo { println("hello") }        // 使用兩個默認值 bar = 0 與 baz = 1

具名參數

可以在調用函數時使用具名的函數參數。當一個函數有大量的參數或默認參數時這會非常方便。
給定以下函數:

fun reformat(str: String,
             normalizeCase: Boolean = true,
             upperCaseFirstLetter: Boolean = true,
             divideByCamelHumps: Boolean = false,
             wordSeparator: Char = ' ') {
/*……*/
}

我們可以使用默認參數來調用它:

reformat(str)

然而,當使用非默認參數調用它時,該調用看起來就像:

reformat(str, true, true, false, 'a')

使用具名參數我們可以使代碼更具有可讀性:

reformat(str,
    normalizeCase = true,
    upperCaseFirstLetter = true,
    divideByCamelHumps = false,
    wordSeparator = '_'
)

並且如果我們不需要所有的參數:

reformat(str, wordSeparator = '_')

當一個函數調用混用位置參數與具名參數時,所有位置參數都要放在第一個具名參數之前。例如,允許調用 f(1, y = 2) 但不允許 f(x = 1, 2)。
可以通過使用星號操作符將可變數量參數(vararg) 以具名形式傳入:

fun foo(vararg strings: String) { /*……*/ }
foo(strings = *arrayOf("a", "b", "c"))

返回 Unit 的函數

如果一個函數不返回任何有用的值,它的返回類型是 Unit。Unit 是一種只有一個值——Unit 的類型。這個值不需要顯式返回:

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")
    // `return Unit` 或者 `return` 是可選的
}

Unit 返回類型聲明也是可選的。上面的代碼等同於:

fun printHello(name: String?) { …… }

單表達式函數

當函數返回單個表達式時,可以省略花括號並且在 = 符號之後指定代碼體即可:

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

當返回值類型可由編譯器推斷時,顯式聲明返回類型是可選的:

fun double(x: Int) = x * 2

顯式返回類型

具有塊代碼體的函數必須始終顯式指定返回類型,除非他們旨在返回 Unit,在這種情況下它是可選的。 Kotlin 不推斷具有塊代碼體的函數的返回類型,因爲這樣的函數在代碼體中可能有複雜的控制流,並且返回類型對於讀者(有時甚至對於編譯器)是不明顯的。

可變數量的參數(Varargs)

函數的參數(通常是最後一個)可以用 vararg 修飾符標記:

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

允許將可變數量的參數傳遞給函數:

val list = asList(1, 2, 3)

只有一個參數可以標註爲 vararg。如果 vararg 參數不是列表中的最後一個參數, 可以使用具名參數語法傳遞其後的參數的值,或者,如果參數具有函數類型,則通過在括號外部傳一個 lambda。

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

val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

中綴表示法

標有 infix 關鍵字的函數也可以使用中綴表示法(忽略該調用的點與圓括號)調用。中綴函數必須滿足以下要求:

  • 它們必須是成員函數或擴展函數;
  • 它們必須只有一個參數;
  • 其參數不得接受可變數量的參數且不能有默認值。
infix fun Int.shl(x: Int): Int { …… }
// 用中綴表示法調用該函數
1 shl 2
// 等同於這樣
1.shl(2)

中綴函數調用的優先級低於算術操作符、類型轉換以及 rangeTo 操作符。 以下表達式是等價的:

1 shl 2 + 3 等價於 1 shl (2 + 3)
0 until n * 2 等價於 0 until (n * 2)
xs union ys as Set<*> 等價於 xs union (ys as Set<*>)

另一方面,中綴函數調用的優先級高於布爾操作符 && 與 ||、is- 與 in- 檢測以及其他一些操作符。這些表達式也是等價的:

a && b xor c 等價於 a && (b xor c)
a xor b in c 等價於 (a xor b) in c

請注意,中綴函數總是要求指定接收者與參數。當使用中綴表示法在當前接收者上調用方法時,需要顯式使用 this;不能像常規方法調用那樣省略。這是確保非模糊解析所必需的。

class MyStringCollection {
    infix fun add(s: String) { /*……*/ }
    
    fun build() {
        this add "abc"   // 正確
        add("abc")       // 正確
        //add "abc"        // 錯誤:必須指定接收者
    }
}

函數作用域

在 Kotlin 中函數可以在文件頂層聲明,這意味着你不需要像一些語言如 Java、C# 或 Scala 那樣需要創建一個類來保存一個函數。此外除了頂層函數,Kotlin 中函數也可以聲明在局部作用域、作爲成員函數以及擴展函數。

局部函數

Kotlin 支持局部函數,即一個函數在另一個函數內部:

fun dfs(graph: Graph) {
    fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v, visited)
    }

    dfs(graph.vertices[0], HashSet())
}

局部函數可以訪問外部函數(即閉包)的局部變量,所以在上例中,visited 可以是局部變量:

fun dfs(graph: Graph) {
    val visited = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v)
    }

    dfs(graph.vertices[0])
}

成員函數

成員函數是在類或對象內部定義的函數:

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

成員函數以下方法調用:

Sample().read() // 創建類 Stream 實例並調用 read()
//或者
Sample()::read 

泛型函數

函數可以有泛型參數,通過在函數名前使用尖括號指定:

fun <T> singletonList(item: T): List<T> { /*……*/ }

尾遞歸函數

Kotlin 支持一種稱爲尾遞歸的函數式編程風格。 這允許一些通常用循環寫的算法改用遞歸函數來寫,而無堆棧溢出的風險。 當一個函數用 tailrec 修飾符標記並滿足所需的形式時,編譯器會優化該遞歸,留下一個快速而高效的基於循環的版本:

val eps = 1E-10 // "good enough", could be 10^-15

tailrec fun findFixPoint(x: Double = 1.0): Double
        = if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))

這段代碼計算餘弦的不動點(fixpoint of cosine),這是一個數學常數。 它只是重複地從 1.0 開始調用 Math.cos,直到結果不再改變,對於這裏指定的 eps 精度會產生 0.7390851332151611 的結果。最終代碼相當於這種更傳統風格的代碼:

val eps = 1E-10 // "good enough", could be 10^-15

private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (Math.abs(x - y) < eps) return x
        x = Math.cos(x)
    }
}

要符合 tailrec 修飾符的條件的話,函數必須將其自身調用作爲它執行的最後一個操作。在遞歸調用後有更多代碼時,不能使用尾遞歸,並且不能用在 try/catch/finally 塊中。目前在 Kotlin for JVM 與 Kotlin/Native 中支持尾遞歸。

尾巴

今天的學習筆記就先到這裏了,下一篇我們將學習Kotlin中的高階函數和Lambda表達式
老規矩,喜歡我的文章,歡迎素質三連:點贊,評論,關注,謝謝大家!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章