從Java到Kotlin(五)

陳子豪發表於2018-02-27

函式與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到Kotlin(五)
而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

從Java到Kotlin(五)

相關文章