函式
Kotlin 函式使用 fun
關鍵字宣告:
fun double(x: Int): Int {
return 2 * x
}
函式使用
函式以標準方式呼叫:
val result = double(2)
呼叫成員函式使用點符號:
Stream().read() // create instance of class Stream and call read()
參數
函式參數使用 Pascal 表示法定義 - 名稱: 類型。參數之間以逗號分隔,且每個參數都必須明確指定類型:
fun powerOf(number: Int, exponent: Int): Int { /*...*/ }
宣告函式參數時,您可以使用結尾逗號:
fun powerOf(
number: Int,
exponent: Int, // trailing comma
) { /*...*/ }
預設引數
函式參數可以有預設值,當您省略對應的引數時會使用這些預設值。這減少了多載的數量:
fun read(
b: ByteArray,
off: Int = 0,
len: Int = b.size,
) { /*...*/ }
預設值透過在類型後附加 =
來設定。
覆寫方法始終使用基礎方法的預設參數值。 當覆寫一個具有預設參數值的方法時,預設參數值必須從簽章中省略:
open class A {
open fun foo(i: Int = 10) { /*...*/ }
}
class B : A() {
override fun foo(i: Int) { /*...*/ } // No default value is allowed.
}
如果一個預設參數位於沒有預設值的參數之前,那麼預設值只能透過使用具名引數呼叫函式來使用:
fun foo(
bar: Int = 0,
baz: Int,
) { /*...*/ }
foo(baz = 1) // The default value bar = 0 is used
如果預設參數後的最後一個引數是 lambda 運算式,您可以將其作為具名引數傳遞,或放在括號外傳遞:
fun foo(
bar: Int = 0,
baz: Int = 1,
qux: () -> Unit,
) { /*...*/ }
foo(1) { println("hello") } // Uses the default value baz = 1
foo(qux = { println("hello") }) // Uses both default values bar = 0 and baz = 1
foo { println("hello") } // Uses both default values bar = 0 and baz = 1
具名引數
您可以在呼叫函式時為其一個或多個引數命名。當函式有許多引數且難以將值與引數關聯時,這會很有幫助,尤其是當它是布林值或 null
值時。
當您在函式呼叫中使用具名引數時,您可以自由更改其列出的順序。如果您想使用它們的預設值,您可以完全省略這些引數。
考慮 reformat()
函式,它有 4 個具有預設值的引數。
fun reformat(
str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ',
) { /*...*/ }
呼叫此函式時,您不必為所有引數命名:
reformat(
"String!",
false,
upperCaseFirstLetter = false,
divideByCamelHumps = true,
'_'
)
您可以跳過所有具有預設值的引數:
reformat("This is a long String!")
您也可以跳過特定具有預設值的引數,而不是全部省略。但是,在第一個跳過的引數之後,您必須為所有後續引數命名:
reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')
您可以使用展開運算子(在陣列前加上 *
)傳遞可變數量引數(vararg
)與名稱:
fun foo(vararg strings: String) { /*...*/ }
foo(strings = *arrayOf("a", "b", "c"))
NOTE
當在 JVM 上呼叫 Java 函式時,您不能使用具名引數語法,因為 Java 位元碼 (bytecode) 不總是保留函式參數的名稱。
回傳 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)
在函式內部,類型為 T
的 vararg
參數會顯示為 T
的陣列,如上述範例所示,其中 ts
變數的類型為 Array<out T>
。
只有一個參數可以被標記為 vararg
。如果 vararg
參數不是列表中的最後一個,則後續參數的值必須使用具名引數語法傳遞,或者,如果參數具有函式類型,則透過在括號外傳遞 lambda 運算式。
當您呼叫 vararg
函式時,您可以單獨傳遞引數,例如 asList(1, 2, 3)
。如果您已經有一個陣列並想將其內容傳遞給函式,請使用展開運算子(在陣列前加上 *
):
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
如果您想將基本型別陣列傳遞給 vararg
,您需要使用 toTypedArray()
函式將其轉換為常規(型別化)陣列:
val a = intArrayOf(1, 2, 3) // IntArray is a primitive type array
val list = asList(-1, 0, *a.toTypedArray(), 4)
中綴表示法
標記有 infix
關鍵字的函式也可以使用中綴表示法呼叫(省略呼叫的點和括號)。中綴函式必須滿足以下要求:
infix fun Int.shl(x: Int): Int { ... }
// calling the function using the infix notation
1 shl 2
// is the same as
1.shl(2)
NOTE
中綴函式呼叫的優先順序低於算術運算子、型別轉換和 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" // Correct
add("abc") // Correct
//add "abc" // Incorrect: the receiver must be specified
}
}
函式作用域
Kotlin 函式可以宣告在檔案的頂層,這意味著您不需要建立一個類別來容納一個函式,這在 Java、C# 和 Scala 等語言中是必需的(頂層定義自 Scala 3 起可用)。除了頂層函式,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().foo() // creates instance of class Sample and calls foo
泛型函式
函式可以有泛型參數,這些參數使用尖括號在函式名稱之前指定:
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))
這段程式碼計算了餘弦的不動點,這是一個數學常數。它只是重複呼叫 Math.cos
從 1.0
開始,直到結果不再改變為止,對於指定的 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 的 JVM 和 Kotlin/Native 都支援尾遞迴。
另請參閱: