一、 概述
函數:也就是子程序。
高階函數:在數學和計算機科學中,高階函數是至少滿足下列一個條件的函數:接受一個或多個函數作為輸入,輸出一個函數
在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
}
注:
- 要符合 tailrec 修飾符的要求, 函數必須在它執行的所有操作的最后一步, 遞歸調用它自身
- 如果遞歸調用后還有其他邏輯代碼,不能使用尾遞歸
- 尾遞歸不能用在try/catch/finally 結構內
- 尾遞歸目前只能用在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