函式與Lambda表示式
目錄
一、函式宣告與呼叫
二、引數和返回值
三、單表示式函式
四、函式作用域
五、泛型函式
六、尾遞迴函式
七、中綴表示法
八、Lambda表示式的語法
九、高階函式與Lambda表示式
十、匿名函式
十一、行內函數
一、函式宣告與呼叫
Java 中的方法使用 void 關鍵字宣告:
void foo(){}
複製程式碼
Kotlin 中的函式使用 fun 關鍵字宣告:
fun foo(){}
複製程式碼
用法相似,加入有一個 User 類,裡面有一個 foo() 函式,呼叫函式的程式碼如下: Java程式碼
new User().foo();
複製程式碼
Kotlin程式碼
User().foo()
複製程式碼
二、引數和返回值
宣告有引數的函式,程式碼如下: Java程式碼
void foo(String str, int i) {}
複製程式碼
Kotlin程式碼
fun foo(str: String, i: Int) {}
複製程式碼
Java先定義型別,後命名;Kotlin先命名,後定義型別,中間用冒號:
分隔。兩者都是多個引數中間用逗號,
分隔。
如函式有返回值,程式碼如下:
Java程式碼
String foo(String str, int i) {
return "";
}
複製程式碼
Kotlin程式碼
fun foo(str: String, i: Int): String {
return ""
}
複製程式碼
Java是把void
替換成返回值的型別,而Kotlin是把返回值宣告在函式的末尾,並用冒號:
分隔。
兩種語言宣告引數和返回值的方式有點相似,而Kotlin還有更強大的功能,例如預設引數
和 命名引數
,如下所示:
函式引數可以有預設值,當沒有給引數指定值的時候,使用預設值
//給i指定預設值為1
fun foo(str: String, i: Int = 1) {
println("$str $i")
}
//呼叫該函式,這個時候可以只傳一個引數
foo("abc")
//執行程式碼,得到結果為: abc 1
複製程式碼
如果有預設值的引數在無預設值的引數之前,要略過有預設值的引數去給無預設值的引數指定值,要使用命名引數
來指定值,有點繞我們看程式碼:
//有預設值的引數在無預設值的引數之前
fun foo(i: Int = 1, str: String) {
println("$str $i")
}
//foo("hello") //編譯錯誤
foo(str = "hello") //編譯通過,要使用引數的命名來指定值
//執行程式碼,得到結果為: hello 1
複製程式碼
- 可變數量的引數
函式的引數可以用 vararg 修飾符標記,表示允許將可變數量的引數傳遞給函式,如下所示:
//用 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 a = arrayOf(1, 2, 3)
//*a代表把a裡所有元素
val list = asList(-1, 0, *a, 4)
//執行程式碼,得到結果為: [-1, 0, 1, 2, 3, 4]
複製程式碼
三、單表示式函式
在Kotlin中,如果函式的函式體只有一條語句,並且有返回值,那麼可以省略函式體的大括號,變成單表示式函式。如下所示:
//函式體內只有一條語句,且有返回值
fun foo(): String{
return "abc"
}
//這時可以省略大括號,變成單表示式函式
fun foo() = "abc"
複製程式碼
四、函式作用域
在 Kotlin 中函式可以在檔案頂層宣告,這意味著你不需要像一些語言如 Java 那樣建立一個類來儲存一個函式。此外除了頂層函式,Kotlin 中函式也可以宣告在區域性作用域、作為成員函式以及擴充套件函式。
1. 成員函式
成員函式是指在類或物件裡定義的函式。
Java程式碼:
class User {
//在類裡定義函式。
void foo() {}
}
//呼叫
new User().foo();
複製程式碼
Kotlin程式碼:
class User() {
//在類裡定義函式。
fun foo() {}
}
//呼叫
User().foo()
複製程式碼
2. 區域性函式
Kotlin支援在函式內巢狀另一個函式,巢狀在裡面的函式成為區域性函式,如下所示:
fun foo() {
println("outside")
fun inside() {
println("inside")
}
inside()
}
//呼叫foo()函式
foo()
複製程式碼
執行程式碼,得到結果
而Java中沒有區域性函式這一概念。五、泛型函式
泛型引數使用尖括號指定,如下所示: Java程式碼
<T> void print(T t) {
}
<T> List<T> printList(T t) {
}
複製程式碼
Kotlin程式碼
fun <T> printList(item: T) {
}
fun <T> printList(item: T): List<T> {
}
複製程式碼
六、尾遞迴函式
尾遞迴函式是一個遞迴函式,用關鍵字tailrec
來修飾,函式必須將其自身呼叫作為它執行的最後一個操作。當一個函式用tailrec
修飾符標記並滿足所需的形式時,編譯器會優化該遞迴,留下一個快速而高效的基於迴圈的版本,無堆疊溢位的風險,舉個例子:
先看一段程式碼
fun count(x: Int = 1): Int = if (x == 10) x else count(x - 1)
複製程式碼
上面的count()
函式是一個死迴圈,當我們呼叫count()
函式後,會報StackOverflowError。這時可以用tailrec
修飾符標記該遞迴函式,並將其自身呼叫作為它執行的最後一個操作,如下所示:
tailrec fun count(x: Int = 1): Int = if (x == 10) x else count(x - 1)
複製程式碼
再次執行程式碼,無堆疊溢位。
七、中綴表示法
中綴表示法是呼叫函式的另一種方法。如果要使用中綴表示法,需要用infix
關鍵字來修飾函式,且要滿足下列條件:
- 它們必須是成員函式或擴充套件函式;
- 它們必須只有一個引數;
- 其引數不得接受可變數量的引數。
下面來舉個例子:
//擴充套件函式
infix fun String.removeLetter(str: String): String {
//this指呼叫者
return this.replace(str, "")
}
//呼叫
var str = "hello world"
//不使用中綴表示法
println(str.removeLetter("h")) //輸出ello world
//使用中綴表示法
println(str removeLetter "d") //輸出hello worl
//使用中綴表示法呼叫str removeLetter "d"等同於呼叫str.removeLetter("d")
//還可以連續呼叫
println(str.removeLetter("h").removeLetter("d").removeLetter("l")) // 輸出 eo wor
println(str removeLetter "h" removeLetter "d" removeLetter "l") // 輸出 eo wor
複製程式碼
八、Lambda表示式的語法
Lambda表示式的語法如下:
- Lambda 表示式總是括在大括號中;
- 其引數(如果有的話)在 -> 之前宣告(引數型別可以省略);
- 函式體(如果存在的話)在 -> 後面。
舉個例子:
//這是一個Lambda表示式的完整語法形式
val sum = { x: Int, y: Int -> x + y }
//Lambda表示式在大括號中
//引數 x 和 y 在 -> 之前宣告
//引數宣告放在大括號內,並有引數型別標註
//函式體 x + y 在 -> 後面
val i: Int = sum(1, 2)
println(i) //輸出結果為 3
複製程式碼
如果Lambda表示式自動推斷的返回型別不是Unit
,那麼在Lambda表示式函式體中,會把最後一條表示式的值當做是返回值。所以上面的常量sum
的返回值是Int
型別。如果要指定常量sum
的返回值為Int
型別,可以這樣寫:
val sum: (Int, Int) -> Int = { x, y -> x + y }
val i: Int = sum(1, 2)
println(i) //輸出結果為 3
複製程式碼
當Lambda表示式只有一個引數的時候,那麼它將可以省略這個唯一的引數的定義,連同->
也可以省略。如下所示:
//當Lambda表示式只有一個引數的時候
val getInt: (Int) -> Int = { x -> x + 1 }
val int = getInt(2)
println(int) //輸出結果為:3
//可以省略這個引數的定義
//並且將隱含地獎這個引數命名為 it
val sum: (Int) -> Int = { it + 1 }
val int = sum(2)
println(int) //輸出結果為:3
複製程式碼
上面說到如果Lambda表示式自動推斷的返回型別不是Unit
,那麼在Lambda表示式函式體中,會把最後一條表示式的值當做是返回值。舉個例子:
var sum: (Int) -> Int = {
val i: Int = it + 1
val j: Int = i + 3
val k: Int = it + j - i
i
k
j
}
println(sum(1))
//輸出結果為 5,也就是 j 的值
複製程式碼
九、高階函式與Lambda表示式
高階函式是將函式用作引數或返回值的函式,如下所示:
fun getName(name: String): String {
return name
}
fun printName(a: String, name: (str: String) -> String): String {
var str = "$a${name("Czh")}"
return str
}
//呼叫
println(printName("Name:", ::getName))
//執行程式碼,輸出 Name:Czh
複製程式碼
上面程式碼中name: (str: String) -> String
是一個函式,擁有函式型別() -> String,接收一個String引數,當我們執行var str = "$a${name("Czh")}"
這行程式碼的時候,相當於執行了var str = "$a${getName("Czh")}"
,並返回了字串"Czh"。當我們呼叫printName("Name:", ::getName)
時,將函式作為引數傳入高階函式,需要在該函式前加兩個冒號::
作為標記。
Kotlin提供了Lambda表示式來讓我們更方便地傳遞函式引數值。Lambda表示式總是被大括號括著;如果有引數的話,其引數在 ->
之前宣告,引數型別可以省略;如果存在函式體的話,函式體在->
後面,如下所示:
println(printName("Name:", { name -> getName("Czh") }))
//執行程式碼,輸出 Name:Czh
複製程式碼
如果函式的最後一個引數是一個函式,並且你傳遞一個Lambda表達
式作為相應的引數,你可以在圓括號()
之外指定它,如下所示:
println(printName("Name:") { name -> getName("Czh") })
//執行程式碼,輸出 Name:Czh
複製程式碼
十、匿名函式
匿名函式與常規函式一樣,只是省略了函式名稱而已。舉個例子
fun(x: Int, y: Int): Int = x + y
複製程式碼
匿名函式函式體是表示式,也可以是程式碼段,如下所示:
fun(x: Int, y: Int): Int {
return x + y
}
複製程式碼
上面高階函式的例子中的printName
函式的第二個引數也可以傳入一個匿名函式,如下所示:
println(printName("Name:", fun(str: String): String { return "Czh" }))
//執行程式碼,輸出 Name:Czh
複製程式碼
十一、行內函數
1.行內函數
使用高階函式會帶來一些執行時的效率損失。每一個函式都是一個物件,並且會捕獲一個閉包。 即那些在函式體內會訪問到的變數。 記憶體分配(對於函式物件和類)和虛擬呼叫會引入執行時間開銷。這時可以通過行內函數消除這類的開銷。舉個例子:
fun printName(a: String, name: (str: String) -> String): String {
var str = "$a${name("Czh")}"
return str
}
println(printName("Name:", { name -> getName("Czh") }))
複製程式碼
上面程式碼中,printName
函式有一個函式型別的引數,通過Lambda表示式向printName
函式傳入引數值,Kotlin編譯器會為Lambda表示式單獨建立一個物件,再將Lambda表示式轉換為相應的函式並呼叫。如果這種情況出現比較多的時候,就會很消耗資源。這是可以在函式前使用inline
關鍵字,把Lambda函式內聯到呼叫處。如下所示:
inline fun printName(a: String, name: (str: String) -> String): String {
var str = "$a${name("Czh")}"
return str
}
println(printName("Name:", { name -> getName("Czh") }))
複製程式碼
2.禁用內聯
通過inline
關鍵字,編譯器將Lambda函式內聯到呼叫處,消除了執行時消耗。但內聯可能導致生成的程式碼增加,所以需要避免內聯比較大的Lambda表示式。如果想禁用一些Lambda函式的內聯,可以使用noinline
修飾符禁用該Lambda函式的內聯,如下所示:
inline fun printName(name1: (str1: String) -> String
, noinline name2: (str2: String) -> String): String {
var str = "${name1("Name:")}${name2("Czh")}"
return str
}
複製程式碼
3.內聯屬性
inline
關鍵字除了可以使函式內聯之外,還能內聯沒有幕後欄位(field)的屬性,如下所示:
val foo: Foo
inline get() = Foo()
var bar: Bar
get() = ……
inline set(v) { …… }
複製程式碼
總結
本篇文章對比了Java方法和Kotlin函式在寫法上的區別,也認識了Lambda函式和還列舉了一些Kotlin函式中比較特別的語法,如中綴表示法等。可見Kotlin中的函式內容還是很多的,用法也相對複雜,但運用好Kotlin的函式,能使開發變得更簡單。
參考文獻:
Kotlin語言中文站、《Kotlin程式開發入門精要》
推薦閱讀:
從Java到Kotlin(一)為什麼使用Kotlin
從Java到Kotlin(二)基本語法
從Java到Kotlin(三)類和介面
從Java到Kotlin(四)物件與泛型
從Java到Kotlin(五)函式與Lambda表示式
從Java到Kotlin(六)擴充套件與委託
從Java到Kotlin(七)反射和註解
從Java到Kotlin(八)Kotlin的其他技術
Kotlin學習資料總彙
更多精彩文章請掃描下方二維碼關注微信公眾號"AndroidCzh":這裡將長期為您分享原創文章、Android開發經驗等! QQ交流群: 705929135