1. 程式人生 > 實用技巧 >Kotlin進階學習2

Kotlin進階學習2

寫在前面

本文緊接上文:Kotlin進階學習1。在之前我們學習了一些Kotlin的特性,這次接著來學習Koltin中一些有用的特性

擴充套件函式

介紹

擴充套件函式是什麼呢?擴充套件函式表示在即使不修改某個類的原始碼的情況下,仍然可以開啟這個類,向該類新增新的函式。

引入和使用

看起來似乎比較難以理解,我們還是結合一個例子來說明——統計字串中的字母數量,這段程式碼是很容易寫的,只要編寫一個單例類,然後定義一個lettersCount()函式,傳入字串,然後在裡面處理並返回字母數量就好了,如下:

object StringUtil{
    fun lettersCount(str:String):Int{
        var count = 0
        for(char in str){
            if(char.isLetter()){
                count++
            }
        }
        return count
    }
}

要使用這段程式碼,也很方便:

val str = "ABC123XYZ!@"
val count = StringUtil.lettersCount(str)

但有了擴充套件函式,我們可以用一種更加面向物件的思維來實現這個功能,比如把letterCount()函式新增到String類當中。

我們先來學習一下擴充套件函式的語法結構:

fun ClassName.methodName(param1:Int,param2:Int):Int{}

相比於定義一個普通的函式,只需要在函式名前加一個ClassName.的語法結構就可以新增到對應的類當這種去了。

接下來就來使用一下,需要注意的是我們建議把擴充套件函式定義成頂層方法,這樣就可以作用到全域性了。

比如剛才的例子,我們把這個函式定義到String類中:

    fun String.lettersCount():Int{
        var count = 0
        for(char in this){
            if(char.isLetter()){
                count++
            }
        }
        return count
    }

這裡定義成了String類的擴充套件函式,那麼函式內部自然就有了String例項的上下文,這裡的this就是指的這個String例項了。

此時,要使用這個擴充套件函式就更簡單了:

val count = "ABC123456".lettersCount()

看上去就好像是String類自帶了這個方法一樣。

使用擴充套件函式,可以讓API變得更加簡潔,豐富,並且更加地面向物件。最後提一句,你可以向任何類中新增拓展函式,Kotlin對此沒有限制。

運算子過載

介紹

記得我第一次學習C++的時候,對C++中的運算子過載機制覺得十分驚訝。居然連運算子都可以過載。非常好的是我們的Kotlin也支援運算子過載, 但需要注意的是,不同的運算子對應的過載函式是不同的,比如加號對應的是plus(),減號對應的是minus()函式,在過載函式前加上operator關鍵字就可以編寫裡面的邏輯了:

class Obj{
    operator fun plus(obj:Obj):Obj{
        // 處理邏輯
    }
}

使用

接下來,我們就來實現一個有意義的事情:讓兩個Money物件相加。

首先定義一個Money類:

class Money(val value:Int)

在Money類中過載加法:

class Money(val value:Int){
    operator fun plus(money:Money):Money{
        val sum = value + money.value
        return Money(sum)
    }
}

可以看到,這段程式碼並不複雜,就不過多解釋了。我們來使用一下:

val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2

但是Money物件只能和Money物件相加,未免有點不方便。如果能直接和數字相加的話就更好了。這個功能當然也是可以實現的,因為Kotlin允許多重過載:

class Money(val value:Int){
    operator fun plus(money:Money):Money{
        val sum = value + money.value
        return Money(sum)
    }
    operator fun plus(newValue:Int):Money{
        val sum = value + newValue
        return Money(sum)
    }
}

要使用也很簡單:

val money4 = money3 + 50

語法糖表示式與實際函式對應表

Kotlin允許我們過載的運算子和關鍵字多達十幾個,具體的表如下:

語法糖表示式 實際呼叫函式
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a++ a.inc()
a-- a.dec()
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a == b a.equals(b)
a > b
a < b
a >= b a.compareTo(b)
a <= b
a..b a.rangeTo(b)
a[b] a.get(b)
a[b] = c a.set(b,c)
a in b b.contains(a)

需要過載的時候可以進行查表,不必記住全部。

高階函式

介紹

在之前的學習中,我們學習了很多函式式API的用法,比如map,filter,run等等。這些函式的特點就是可以傳入一個Lambda表示式作為引數。像這種接收Lambda引數的函式就可以稱為具有函數語言程式設計風格的API,而如果想定義自己的函式式API,就需要藉助高階函數了。

所謂高階函式,指的是如果一個函式接收另一個函式作為引數,或者返回值的型別是另一個函式,那麼該函式就被稱作高階函式。

這個概念乍一看十分奇怪,函式怎麼能接收函式作為引數呢?其實在Kotlin中,除了整型,布林型等等,還定義了函式型別。如下格式:

(String,Int) -> Unit

既然是定義函式型別,那麼關鍵的就是要宣告接收什麼引數,以及返回值。->左邊的部分就是用來宣告接收的引數的,多個引數用逗號隔開,如果不接收則只寫一對括號就好了。右邊則是用於宣告函式的返回值,如果沒有返回值就寫Unit,大概相當於Java中的void。

有了函式型別的定義後,我們就可以定義高階函數了:

fun example(func:(String,Int) -> Unit){
    func("hello",123)
}

那麼高階函式到底有什麼用呢?總的來說就是高階函式執行讓函式型別的引數來決定函式的執行邏輯。接下來我們實踐一下。

使用函式引用來使用

這裡我們定義一個叫做num1AndNum2()的高階函式,用來處理兩個數字之間的某種運算,然後返回最終結果:

fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int) -> Int) : Int{
    val result = operation(num1,num2)
    return result
}

具體來看,這裡的前兩個引數不解釋了。第三個引數,我們定義了一個名字叫做operation的函式型別引數,這個引數接收兩個Int型的引數,且返回一個Int型的結果。我們在具體實現邏輯裡沒有進行實際的運算,而是交給了operation來運算,並返回結果。

那麼我們怎麼使用?如下:

fun plus(num1:Int,num2:Int):Int{
    return num1 + num2
}
fun main(){
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1,num2,::plus)
    println(result1)
}

為了使用這個高階函式,我們必須先定義一個與他定義的函式型別匹配的函式才行。於是我們定義了一個plus函式,用來處理加法。之後我們在主函式裡使用這個高階函式,其中::plus代表函式引用,表示將plus()函式當作引數傳遞給num1AndNum2()函式。這裡其實就是使用了plus函式的邏輯去決定了具體的運算邏輯。

但這種寫法,每次使用高階函式還得先定義一個普通函式,未免太過於複雜了。實際上Koltin中提供了很多種呼叫高階函式的方式,比如Lambda表示式、匿名函式、成員引用等等。其中Lambda表示式是最常用的用法了。所以接下來我們重點學習這部分。

使用Lambda表示式來使用

上面的程式碼如果用Lambda表示式來寫的話,如下:

fun main(){
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1,num2){
        n1,n2 -> n1 + n2
    }
    println(result1)
}

可以看到,之前的程式碼變得十分精簡了。我們也可以把剛才定義的plus()函式刪掉了。

為了加深理解,我們接下來再做一個例子,嘗試用高階函式實現類似apply函式的功能。典型的就是StringBuilder,我們可以定義一個StringBuilder的擴充套件函式:

fun StringBuilder.build(block:StringBuilder.() -> Unit) :StringBuilder{
    block()
    return this
}

注意,這裡的函式型別宣告方式和之前不同。其實這才是定義高階函式最完整的語法,在函式型別的前面加上ClassName代表這個函式型別是定義在哪個類中的。那麼這樣寫有什麼好處呢?好處就是當我們呼叫build函式時,傳入的Lambda表示式將會自動擁有StringBuilder的上下文,就跟apply函式很像了吧?那麼趕緊來試試:

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear")
    val result = StringBuilder().build{
        append("Start eating fruits")
        for (fruit in list){
            append(fruit).append("\n")
        }
        append("Ate all things")
    }
    println(result.toString())
}

可以看到,這個build函式和apply函式實現了類似的功能,不過這裡的build函式暫時只能作用於StringBuilder類上面,等我們學習了泛型後就可以作用於所有類上了。

行內函數

介紹

要了解行內函數的定義和作用,就要先來看看高階函式的具體實現。剛才我們學習了高階函式,可以看到十分的方便,傳入一個Lambda表示式就可以了。但Koltin的程式碼還是要編譯成Java位元組碼的,Java又不支援高階函式,那是怎麼實現的呢?其實Kotlin的編譯器會將高階函式轉換成類似於匿名內部類的實現方式。這也就代表每呼叫一次Lambda表示式,都會建立一個新的匿名類例項,當然會造成額外的記憶體和效能開銷。

為了解決這個問題,Kotlin提供了行內函數的功能,它可以將Lambda表示式時帶來的執行開銷完全消除。

使用

要使用十分簡單,加上inline關鍵字即可:

inline fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int) -> Int) : Int{
    val result = operation(num1,num2)
    return result
}

那麼工作原理又是如何呢?其實就是Kotlin編譯器會把行內函數中的程式碼在編譯的時候自動替換到呼叫它的地方。這樣一來執行時的開銷也就消除了。

noinline

但是,考慮一些特殊情況,比如一個高階函式接收了兩個或更多的函式型別的引數,但我們只想內聯其中的一個怎麼辦呢?這時就可以使用noinline關鍵字了:

inline fun inlineTest(block1:() -> Unit,noinline block2: () -> Unit){
    
}

這樣,只有block1()會內聯,而block2()則不會。行內函數與非行內函數,有一個重要區別,那就是行內函數內引用的Lambda表示式是可以使用return 關鍵字來進行函式返回的,但非行內函數卻不能。

crossinline

將高階函式宣告成行內函數是一種良好的程式設計習慣。事實上,絕大多數的高階函式都是可以直接宣告成行內函數的。但在有些情況下,卻有例外:

inline fun runRunnable(block:() -> Unit){
    val runnable = Runnable{
        block()
    }
    runnable.run()
}

這段程式碼在加上了inline關鍵字後,就無法正常工作了。為什麼呢?這裡要解釋起來可能有些複雜。首先,在runRunnable()函式中,我們建立一個Runnable物件,並在Runnable的Lambda表示式裡呼叫了傳入的函式型別引數。而Lambda表示式在編譯的時候會被轉換成匿名類的實現方式,也就是說上述程式碼實際上是在匿名內部類中呼叫了傳入的函式型別引數。

而行內函數所引用的Lambda表示式允許使用return關鍵字進行返回,但由於我們是在匿名類中呼叫的函式型別引數,此時是不可能進行外層呼叫函式返回的,最多隻能對匿名類中的函式呼叫進行返回,因此就出了上面的錯誤。

總的來說,如果我們在高階函式中建立了另外的Lambda表示式或者匿名類的實現,並且在這些實現中呼叫函式型別引數,就無法宣告為行內函數。

要解決這個問題,可以使用crrosinline關鍵字:

inline fun runRunnable(crossinline block:() -> Unit){
    val runnable = Runnable{
        block()
    }
    runnable.run()
}

這個crossinline關鍵字,在這裡的作用就像是提供了一個契約,保證在行內函數的Lambda表示式中一定不會使用return關鍵字了。聲明瞭之後,就無法在呼叫runRunnable函式時的Lambda表示式中使用return關鍵字進行函式返回了。總的來說,除了在return關鍵字上使用有所區別外,crossinline保留了行內函數的其他所有特性。

總結

總的來說,學習了很多知識。其中高階函式要明顯難理解,日後會多敲來加強理解。