函數
Kotlin 函數使用 fun
關鍵字宣告:
fun double(x: Int): Int {
return 2 * x
}
函數使用方式
函數呼叫使用標準方式:
val result = double(2)
呼叫成員函數使用點標記法:
Stream().read() // create instance of class Stream and call read()
參數
函數參數使用帕斯卡標記法定義 - 名稱: 型別。參數之間使用逗號分隔,且每個參數都必須明確指定型別:
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 = '_')
您可以使用「展開 (spread)」運算子(在陣列前加上 *
)傳遞帶名稱的可變數量引數 (vararg
):
fun foo(vararg strings: String) { /*...*/ }
foo(strings = *arrayOf("a", "b", "c"))
當在 JVM 上呼叫 Java 函數時,您不能使用命名引數語法,因為 Java 位元碼不總是保留函數參數的名稱。
返回 Unit
的函數
如果函數不返回任何有用值,其返回型別是 Unit
。Unit
是一種只有一個值 — Unit
的型別。這個值不必明確返回:
fun printHello(name: String?): Unit {
if (name != null)
println("Hello $name")
else
println("Hi there!")
// `return Unit` or `return` is optional
}
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)
中綴函數呼叫的優先級低於算術運算子、型別轉換和
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))
此程式碼計算餘弦的 fixpoint
(不動點),這是一個數學常數。它只是從 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 的 JVM 和 Kotlin/Native 都支援尾遞歸。
另請參閱: