Kotlin教程(二)函數(shù)

寫在開頭:本人打算開始寫一個(gè)Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kotlin的同學(xué)。系列文章的知識點(diǎn)會以《Kotlin實(shí)戰(zhàn)》這本書中順序編寫,在將書中知識點(diǎn)展示出來同時(shí),我也會添加對應(yīng)的Java代碼用于對比學(xué)習(xí)和更好的理解。

Kotlin教程(一)基礎(chǔ)
Kotlin教程(二)函數(shù)
Kotlin教程(三)類、對象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運(yùn)算符重載及其他約定
Kotlin教程(八)高階函數(shù)
Kotlin教程(九)泛型


在Kotlin中創(chuàng)建集合

上一章我們已經(jīng)使用setOf 函數(shù)創(chuàng)建一個(gè)set了。同樣的,我們也可以用類似的方法創(chuàng)建一個(gè)list或者map:

val set = setOf(1, 2, 3)
val list = listOf(1, 2, 3)
val map = mapOf(1 to "one", 2 to "two")

to 并不是一個(gè)特殊的結(jié)構(gòu),而是一個(gè)普通函數(shù),在后面會繼續(xù)探討它。
有沒有想過這里創(chuàng)建出來的set、list、map到底是什么類型的那?可以通過.javaClass 屬性獲取類型,相當(dāng)于Java中的getClass() 方法:

println(set.javaClass)
println(list.javaClass)
println(map.javaClass)

//輸出
class java.util.LinkedHashSet
class java.util.Arrays$ArrayList
class java.util.LinkedHashMap

可以看到都是標(biāo)準(zhǔn)的Java集合類,Kotlin沒有自己專門的集合類,是為了更容易與Java代碼交互,當(dāng)從Kotlin中調(diào)用Java函數(shù)的時(shí)候,不用轉(zhuǎn)換它的集合類來匹配Java的類,反之亦然。
盡管Kotlin的集合類和Java的集合類完全一致,但Kotlin還不止于此。舉個(gè)例子,可以通過以下方法來獲取一個(gè)列表中最后一個(gè)元素,或者得到一個(gè)數(shù)字列表的最大值:

val strings = listOf("first", "second", "fourteenth")
println(strings.last())
val numbers = setOf(1, 14, 2)
println(numbers.max())

//輸出
fourteenth
14

或許你應(yīng)該知道last()max() 在Java的集合類中并不存在,這應(yīng)該是Kotlin自己擴(kuò)展的方法,可以你要知道上面我們打印出來的類型明確是Java中的集合類,但在這里調(diào)用方法的對象就是這些集合類,又是怎么做到讓一個(gè)Java中的類調(diào)用它本身沒有的方法那?在后面我們講到擴(kuò)展函數(shù)的時(shí)候你就會知道了!

讓函數(shù)更好調(diào)用

現(xiàn)在我們知道了如何創(chuàng)建一個(gè)集合,接下來讓我們打印它的內(nèi)容。Java的集合都有一個(gè)默認(rèn)的toString 實(shí)現(xiàn),但它的何世華的輸出是固定的,而且往往不是你需要的樣子:

val list = listOf(1, 2, 3)
println(list) //觸發(fā)toString的調(diào)用

//輸出
[1, 2, 3]

假設(shè)你需要用分號來分隔每一個(gè)元素,然后用括號括起來,而不是采用默認(rèn)實(shí)現(xiàn)。要解決這個(gè)問題,Java項(xiàng)目會使用第三方庫,比如Guava和Apache Commons,或者是在這個(gè)項(xiàng)目中重寫打印函數(shù)。在Kotlin中,它的標(biāo)準(zhǔn)庫中有一個(gè)專門的函數(shù)來處理這種情況。
但是這里我們先不借助Kotlin的工具,而是自己寫實(shí)現(xiàn)函數(shù):

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator) //不用再第一個(gè)元素前添加分隔符
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

這個(gè)函數(shù)是泛型,它可以支持元素為任意類型的集合。讓我們來驗(yàn)證一下,這個(gè)函數(shù)是否可行:

val list = listOf(1, 2, 3)
println(joinToString(list, ";", "(", ")"))

//輸出
(1;2;3)

看來是可行的,接下來我們要考慮的是如何修改讓這個(gè)函數(shù)的調(diào)用更加簡潔呢?畢竟每次調(diào)用都要傳入四個(gè)參數(shù)也是挺麻煩的。

命名參數(shù)

我們關(guān)注的第一個(gè)問題就是函數(shù)的可讀性。就以joinToString 來看:

joinToString(list, "", "", "")

你能看得出這些String都對應(yīng)什么參數(shù)嗎?可能必須要借助IDE工具或者查看函數(shù)說明或者函數(shù)本身才能知道這些參數(shù)的含義。
在Kotlin中,可以做的更優(yōu)雅:

println(joinToString(list, separator = "", prefix = "", postfix = ""))

當(dāng)你調(diào)用一個(gè)Kotlin定義的函數(shù)時(shí),可以顯示得標(biāo)明一些參數(shù)的名稱。如果在調(diào)用一個(gè)函數(shù)時(shí),指明了一個(gè)參數(shù)的名稱,為了避免混淆,那它之后的所有參數(shù)都需要標(biāo)明名稱。

ps: 當(dāng)你在Kotlin中調(diào)用Java定義的函數(shù)時(shí),不能采用命名參數(shù)。因?yàn)榘褏?shù)名稱存到 .class文件是Java8以及更高版本的一個(gè)可選功能,而Kotlin需要保持和Java6的兼容性。

可能到這里你只是覺得命名參數(shù)讓函數(shù)便于理解,但是調(diào)用變得復(fù)雜了,我還得多寫參數(shù)的名稱!別急,與下面說的默認(rèn)參數(shù)相結(jié)合時(shí),你就知道命名參數(shù)的好了。

默認(rèn)參數(shù)值

Java的另一個(gè)普遍存在問題是:一些類的重載函數(shù)實(shí)在太多了。這些重載大多是為了向后兼容,方便API的使用者,最終導(dǎo)致的結(jié)果是重復(fù)。
在Kotlin中,可以在聲明函數(shù)的時(shí)候,指定參數(shù)的默認(rèn)值,這樣可以避免創(chuàng)建重載的函數(shù)。讓我們嘗試改進(jìn)一下前面的joinToString 函數(shù)。在大多數(shù)情況下,我們可能只會改變分隔符或者改變前后綴,所以我們把這些設(shè)置為默認(rèn)值:

fun <T> joinToString(
        collection: Collection<T>,
        separator: String = ",",
        prefix: String = "",
        postfix: String = ""
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator) //不用再第一個(gè)元素前添加分隔符
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

現(xiàn)在在調(diào)用一下這個(gè)函數(shù),可以省略掉有默認(rèn)值的參數(shù),效果就像在Java中聲明的重載函數(shù)一樣。

println(joinToString(list))
println(joinToString(list, ";"))

//輸出
1,2,3
1;2;3

當(dāng)你使用常規(guī)的調(diào)用語法時(shí),必須按照函數(shù)申明中定義的參數(shù)順序來給定參數(shù),可以省略的只有排在末尾的參數(shù)。如果使用命名參數(shù),可以省略中的一些參數(shù),也可以以你想要的任意 順序只給定你需要的參數(shù):

//打亂了參數(shù)順序,并且separator參數(shù)使用了默認(rèn)值
println(joinToString(prefix = "{", collection = list, postfix = "}"))

//輸出
{1,2,3}

注意,參數(shù)的默認(rèn)值是被編譯到被調(diào)用的函數(shù)中,而不是調(diào)用的地方。如果你改變了參數(shù)默認(rèn)值并重新編譯這個(gè)函數(shù),沒有給參數(shù)重新賦值的調(diào)用者,將會開始使用新的默認(rèn)值。

Java沒有參數(shù)默認(rèn)值的概念,當(dāng)你從Java中調(diào)用Kotlin函數(shù)的時(shí)候,必須顯示得指定所有參數(shù)值。如果需要從Java代碼中調(diào)用也能更簡便,可以使用@JvmOverloads注解函數(shù)。這個(gè)指示編譯器生成Java的重載函數(shù),從最后一個(gè)開始省略每個(gè)函數(shù)。例如joinToString函數(shù),編譯器就會生成如下重載函數(shù):
public static final String joinToString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix)
public static final String joinToString(@NotNull Collection collection, @NotNull String separator)
public static final String joinToString(@NotNull Collection collection)
因此,當(dāng)你項(xiàng)目同時(shí)存在Java和Kotlin時(shí),對有默認(rèn)參數(shù)值的函數(shù)習(xí)慣性地加上@JvmOverloads注解是個(gè)不錯(cuò)的做法。

消除靜態(tài)工具類:頂層函數(shù)和屬性

Java作為一門面對對象的語言,需要所有的代碼都寫作類的函數(shù)。但實(shí)際上項(xiàng)目中總有一些函數(shù)不屬于任何一個(gè)類,最終產(chǎn)生了一些類不包含任何狀態(tài)或者實(shí)例函數(shù),僅僅是作為一堆靜態(tài)函數(shù)的容器。在JDK中,最明顯的例子應(yīng)該就是Collections了,還有你的項(xiàng)目中是不是有很多以Util作為后綴的類?
在Kotlin中,根本不需要去創(chuàng)建這些無意義的類,你可以把這些函數(shù)直接放到代碼文件的頂層,不用從屬于任何類。事實(shí)上joinToString函數(shù)之前就是直接定義在Join.kt 文件。

package com.huburt.imagepicker

@JvmOverloads
fun <T> joinToString(...): String {...}

這會怎樣運(yùn)行呢?當(dāng)編譯這個(gè)文件的時(shí)候,會生成一些類,因?yàn)镴VM只能執(zhí)行類中的代碼。當(dāng)你在使用Kotlin的時(shí)候,知道這些就夠了。但是如果你需要從Java中來調(diào)用這些函數(shù),你就必須理解它將怎樣被編譯,來看下編譯后的類是怎樣的:

package com.huburt.imagepicker

public class JoinKt {
    public static String joinToString(...){...}
}

可以看到Kotlin編譯生成的類的名稱,對應(yīng)于包含函數(shù)的文件名稱,這個(gè)文件中的所有頂層函數(shù)編譯為這個(gè)類的靜態(tài)函數(shù)。因此,當(dāng)從Java調(diào)用這個(gè)函數(shù)的時(shí)候,和調(diào)用任何其他靜態(tài)函數(shù)一樣簡單:

import com.huburt.imagepicker.JoinKt

JoinKt.joinToString(...)

修改文件類名

是不是覺得Kt結(jié)尾的類使用起來很別扭,Kotlin提供了方法改變生成類的類名,只需要為這個(gè)kt文件添加@JvmName的注解,將其放到這個(gè)文件的開頭,位于包名的前面:

@file:JvmName("Join") //指定類名
package com.huburt.imagepicker

@JvmOverloads
fun <T> joinToString(...): String {...}

現(xiàn)在就可以用新的類名調(diào)用這個(gè)函數(shù):

import com.huburt.imagepicker.Join

Join.joinToString(...)

頂層屬性

和函數(shù)一樣,屬性也可以放到文件的頂層。從Java的角度來看就是靜態(tài)屬性,沒啥特別的,而且由于沒有了類的存在,這種屬性用到的機(jī)會也不多。
需要注意的是頂層函數(shù)和其他任意屬性一樣,默認(rèn)是通過訪問器暴露給Java使用的(也就是通過getter和setter方法)。為了方便使用,如果你想要把一個(gè)常量以public static final 的屬性暴露給Java,可以用const 來修飾屬性:

const val TAG = "tag"

這樣就等同與Java的:

public static final String TAG = "tag"

給別人的類添加方法:擴(kuò)展函數(shù)和屬性

Kotlin的一大特色就是可以平滑的與西安歐代碼集成。你可以完全在原有的Java代碼基礎(chǔ)上開始使用Kotlin。對于原有的Java代碼可以不修改源碼的情況下擴(kuò)展功能:擴(kuò)展函數(shù)。這一點(diǎn)是我認(rèn)為Kotlin最強(qiáng)大的地方了。
擴(kuò)展函數(shù)非常簡單,它就是一個(gè)類的成員函數(shù),不過定義在類的外面。為了方便闡述,讓我們添加一個(gè)方法,來計(jì)算一個(gè)字符串的最后一個(gè)字符:

package strings

    //String ->接收者類型               //this ->接收者類型
fun String.lastChar(): Char = this.get(this.length - 1)

你所要做的,就是把你要擴(kuò)展的類或者接口的名稱,放到即將添加的函數(shù)前面,這個(gè)類的名稱被稱為接收者類型;用來調(diào)用這個(gè)擴(kuò)展函數(shù)的那個(gè)對象,叫做接收者對象。
接著就可以像調(diào)用類的普通成員函數(shù)一樣去調(diào)用這個(gè)函數(shù)了:

println("Kotlin".lastChar())
//輸出
n

在這個(gè)例子中,String就是接收者類型,而“Kotlin”就是接收者對象。
在這個(gè)擴(kuò)展函數(shù)中,可以像其他成員函數(shù)一樣用this,也可以像普通函數(shù)一樣省略它:

package strings

fun String.lastChar(): Char = get(length - 1) //省略this調(diào)用string對象其他函數(shù)

導(dǎo)入擴(kuò)展函數(shù)

對于你定義的擴(kuò)展函數(shù),它不會自動的在整個(gè)項(xiàng)目范圍內(nèi)生效。如果你需要使用它,需要進(jìn)行導(dǎo)入,導(dǎo)入單個(gè)函數(shù)與導(dǎo)入類的語法相同:

import strings.lastChar

val c = "Kotlin".lastChar()

當(dāng)然也可以用*表示文件下所有內(nèi)容:import strings.*

另外還可以使用as 關(guān)鍵字來修改導(dǎo)入的類或則函數(shù)的名稱:

import strings.lastChar as last

val c = "Kotlin".last()

在導(dǎo)入的時(shí)候重命名可以解決函數(shù)名重復(fù)的問題。

從Java中調(diào)用擴(kuò)展函數(shù)

實(shí)際上,擴(kuò)展函數(shù)是靜態(tài)函數(shù),它把調(diào)用對象作為函數(shù)的第一個(gè)參數(shù)。在Java中調(diào)用擴(kuò)展函數(shù)和其他頂層函數(shù)一樣,通過.kt文件生成Java類調(diào)用靜態(tài)的擴(kuò)展函數(shù),把接收者對象傳入第一個(gè)參數(shù)即可。例如上面提到的lastChar擴(kuò)展函數(shù)是定義在StringUtil.kt中,在Java中就可以這樣調(diào)用:

char c = StringUtilKt.lastChar("Java")

作為擴(kuò)展函數(shù)的工具函數(shù)

現(xiàn)在我們可以寫一個(gè)joinToString 函數(shù)的終極版本了,它和你在Kotlin標(biāo)準(zhǔn)庫中看到的一模一樣:

@JvmOverloads
fun <T> Collection<T>.joinToString(
        separator: String = ",",
        prefix: String = "",
        postfix: String = ""
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) { //this是接收者對象,即T的集合
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

//使用
println(list.joinToString())
println(list.joinToString(";"))
println(list.joinToString(prefix = "{", postfix = "}"))

將原來的參數(shù)Collection<T>,提出來,作為接收者類型編寫的擴(kuò)展函數(shù),使用方法也像是Collection類的成員函數(shù)一樣了(當(dāng)然Java調(diào)用還是靜態(tài)方法,第一個(gè)參數(shù)傳入Collection對象)。

不可重寫的擴(kuò)展函數(shù)

先來看一個(gè)重寫的例子:

//Kotlin中class默認(rèn)是final的,如果需要繼承需要修飾open,函數(shù)也相同
open class View {
    open fun click() = println("View clicked")
}

class Button : View() { //繼承
    override fun click() = println("Button clicked")
}

當(dāng)你聲明了類型為View的變量,那它可以被賦值為Button類型的對象,因?yàn)锽utton是View的一個(gè)子類。當(dāng)你在調(diào)用這個(gè)變量的一般函數(shù),比如click的時(shí)候,如果Button復(fù)寫了這個(gè)函數(shù),name這里將會調(diào)用到Button中復(fù)寫的函數(shù):

val view: View = Button()
view.click()

//輸出
Button clicked

但是對于擴(kuò)展函數(shù)來說,并不是這樣的。擴(kuò)展函數(shù)并不是類的一部分,它是聲明在類之外的。盡管可以給基類和子類都分別定義一個(gè)同名的擴(kuò)展函數(shù),當(dāng)這個(gè)函數(shù)被調(diào)用時(shí),它會用到哪一個(gè)呢?這里,它是由該變量的靜態(tài)類型所決定的,而不是這個(gè)變量的運(yùn)行時(shí)類型。

fun View.showOff() = println("i'm a view!")

fun Button.showOff() = println("i'm a button!")

val view: View = Button()
view.click()

//輸出
i'm a view!

當(dāng)你在調(diào)用一個(gè)類型為View的變量的showOff函數(shù)時(shí),對應(yīng)的擴(kuò)展函數(shù)會被調(diào)用,盡管實(shí)際上這個(gè)變量現(xiàn)在是一個(gè)Button對象。回想一下,擴(kuò)展函數(shù)會在Java中編譯為靜態(tài)函數(shù),同時(shí)接受值將會作為第一個(gè)參數(shù)。這樣其實(shí)2個(gè)showOff擴(kuò)展函數(shù)就是不同參數(shù)的靜態(tài)函數(shù),

View view = new Button();
XxKt.showOff(view);  //定義在Xx.kt文件中

參數(shù)的類型決定了調(diào)用那個(gè)靜態(tài)函數(shù),想要調(diào)用Button的擴(kuò)展函數(shù),則必須先將參數(shù)轉(zhuǎn)成Button類型才行:XxKt.showOff((Button)view);

因此,擴(kuò)展函數(shù)也是有局限性的,擴(kuò)展函數(shù)是能擴(kuò)展,即定義新的函數(shù),而不能重寫改變原有函數(shù)的實(shí)現(xiàn)(本質(zhì)是一個(gè)靜態(tài)函數(shù))。如果定了一個(gè)類中本身存在成員函數(shù)同名的擴(kuò)展函數(shù),Kotlin種調(diào)用該方法的時(shí)候會如何呢?(Java中沒有這個(gè)顧慮,調(diào)用方式不同)

open class View {
    open fun click() = println("View clicked")
}

fun View.click() = println("擴(kuò)展函數(shù)")

val view = View()
view.click()

//輸出
View clicked

明顯了吧~ 對于有同名成員函數(shù)和擴(kuò)展函數(shù)時(shí),在Kotlin中調(diào)用始終執(zhí)行成員函數(shù)的代碼,擴(kuò)展函數(shù)并不起作用,相當(dāng)于沒有定義。這一點(diǎn)在實(shí)際開發(fā)中需要特別注意了!

擴(kuò)展屬性

擴(kuò)展屬性提供了一種方法,用于擴(kuò)展類的API,可以用來訪問屬性,用的是屬性語法而不是函數(shù)的語法。盡管他們被稱為屬性,但它們可以沒有任何狀態(tài),因?yàn)闆]有合適的地方來存儲它,不可能給現(xiàn)有的Java對象的實(shí)例添加額外的字段。舉個(gè)例子吧:

val String.lastChar: Char
    get() = get(length - 1)

同樣是獲取字符串的最后一個(gè)字符,這次是用擴(kuò)展屬性的方式定義。擴(kuò)展屬性也像接收者的一個(gè)普通成員屬性一樣,這里必須定義getter函數(shù),因?yàn)闆]有支持字段,因此沒有默認(rèn)的getter的實(shí)現(xiàn)。同理,初始化也不可以:因?yàn)闆]有地方存儲初始值。
剛剛定義是一個(gè)val的擴(kuò)展屬性,也可以定義var屬性:

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value) {
        setCharAt(length - 1, value)
    }

還記得上一篇文章的自定義訪問器的內(nèi)容嗎?這里的定義方式與自定義訪問器一致,val 屬性不可變,因此只需要定義getter,而var 屬性可變,所以getter和setter都需要。
可能不是很好理解擴(kuò)展屬性,或者會和真正的屬性混淆,下面列出了擴(kuò)展屬性轉(zhuǎn)換成Java的代碼,你就會比較直觀的理解了。

   public static final char getLastChar(@NotNull String $receiver) {
      return $receiver.charAt($receiver.length() - 1);
   }

   public static final char getLastChar(@NotNull StringBuilder $receiver) {
      return $receiver.charAt($receiver.length() - 1);
   }

   public static final void setLastChar(@NotNull StringBuilder $receiver, char value) {
      $receiver.setCharAt($receiver.length() - 1, value);
   }

和擴(kuò)展函數(shù)是相同的,僅僅是靜態(tài)函數(shù):提供獲取lastChar的功能,這樣的定義方式可以在Kotlin中像使用普通屬性的調(diào)用方式來使用擴(kuò)展屬性,給你一種這是屬性的感覺,但本質(zhì)上在Java中就是靜態(tài)函數(shù)。

處理集合:可變參數(shù)、中綴調(diào)用和庫的支持

擴(kuò)展Java集合的API

val strings = listOf("first", "second", "fourteenth")
println(strings.last())
val numbers = setOf(1, 14, 2)
println(numbers.max())

還記的之前我們使用上面的方式獲取了list的最后一個(gè)元素,以及set中的最大值。到這里你可能已經(jīng)知道了,last()max() 都是擴(kuò)展函數(shù),自己點(diǎn)進(jìn)方法驗(yàn)證一下吧!

可變參數(shù)

如果你也看了listOf 函數(shù)的定義,你一定看到了這個(gè):

public fun <T> listOf(vararg elements: T): List<T>

也就是vararg 關(guān)鍵字,這讓函數(shù)支持任意個(gè)數(shù)的參數(shù)。在Java中同樣的可變參數(shù)是在類型后面跟上... ,上面的方法在Java則是:

public <T> List<T> listOf(T... elements)

但是Kotlin的可變參數(shù)相較于Java還是有點(diǎn)區(qū)別:當(dāng)需要傳遞的參數(shù)已經(jīng)包裝在數(shù)組中時(shí),調(diào)用該函數(shù)的語法,在Java中可以按原樣傳遞數(shù)組,而Kotlin則要求你顯示地解包數(shù)組,以便每個(gè)數(shù)組元素在函數(shù)中能作為單獨(dú)的參數(shù)來調(diào)用。從技術(shù)的角度來講,這個(gè)功能被稱為展開運(yùn)算符,而使用的時(shí)候,不過是在對應(yīng)的參數(shù)前面放一個(gè)*

val array = arrayOf("a", "b")
val list = listOf("c", array)
println(list)
val list2 = listOf<String>("c", *array)
println(list2)

//輸出
[c, [Ljava.lang.String;@5305068a]
[c, a, b]

通過對照可以看到,如果不加* ,其實(shí)是把數(shù)組對象當(dāng)做了集合的元素。加上* 才是將數(shù)組中所有元素添加到集合中。listOf 也可以指定泛型<String> ,你可以嘗試在listOf("c", array) 這里加泛型,第二個(gè)參數(shù)array就會提示類型不正確。

Java中沒有展開,我們也可以調(diào)用Kotlin的listOf函數(shù),該函數(shù)聲明在Collections.kt文件下:

List<String> strings = CollectionsKt.listOf(array);
System.out.println(strings);
//List<String> strings = CollectionsKt.listOf("c", array);//無法編譯

//輸出
[a, b]

Java中可以直接傳入數(shù)組,但是不能同時(shí)傳入單個(gè)元素和數(shù)組。

鍵值對的處理:中綴調(diào)用和解構(gòu)聲明

還記得創(chuàng)建map的方式嗎?

val map = mapOf(1 to "one", 7 to "seven", 52 to "fifty-five")

之前說過to 并不是一個(gè)內(nèi)置的結(jié)構(gòu),而是一種特殊的函數(shù)調(diào)用,被稱為中綴調(diào)用。
在中綴調(diào)用中,沒有添加額外的分隔符,函數(shù)名稱是直接放在目標(biāo)對象名稱和參數(shù)之間的,以下兩種調(diào)用方式是等價(jià)的:

1.to("one")//普通調(diào)用
1 to "one" //中綴調(diào)用

中綴調(diào)用可以與只有一個(gè)參數(shù)的函數(shù)一起使用,換句話說就是只要函數(shù)只有一個(gè)參數(shù),都可以支持在Kotlin中的中綴調(diào)用,無論是普通的函數(shù)還是擴(kuò)展函數(shù)。要允許使用中綴符號調(diào)用函數(shù),需要使用infix 修飾符來標(biāo)記它。例如to 函數(shù)的聲明:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

to函數(shù)會返回一個(gè)Pair類型的對象,Pair是Kotlin標(biāo)準(zhǔn)庫中的類,它是用來表示一對元素。我們也可以直接用Pair的內(nèi)容來初始化兩個(gè)變量:

val (number, name) = 1 to "one"

這個(gè)功能稱之為解構(gòu)聲明,1 to "one" 會返回一個(gè)Pair對象,Pair包含一對元素,也就是1和one,接著又定義了變量(number, name) 分別指向Pair中的1和one。
解構(gòu)聲明特征不止用于Pair。還可以使用map的key和value內(nèi)容來初始化兩個(gè)變量。并且還適用于循環(huán),正如你在使用的withIndex 函數(shù)的joinToString 實(shí)現(xiàn)中看到的:

for ((index, element) in collection.withIndex()) {
    printLn("$index, $element")
}

to 函數(shù)是一個(gè)擴(kuò)展函數(shù),可以創(chuàng)建一對任何元素,這意味著它是泛型接受者的擴(kuò)展:可以使用1 to "one""one" to 1list to list.size()等寫法。我們來看看mapOf 函數(shù)的聲明:

public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>

listOf 一樣,mapOf 接受可變數(shù)量的參數(shù),但這次他們應(yīng)該是鍵值對。盡管在Kotlin中創(chuàng)建map可能看起來像特殊的解構(gòu),而它不過是一個(gè)具有簡明語法的常規(guī)函數(shù)。

字符串和正則表達(dá)式的處理

Kotlin定義了一系列擴(kuò)展函數(shù),使標(biāo)準(zhǔn)Java字符串使用起來更加方便。

分割字符串

Java中我們會使用String的split方法分割字符串。但有時(shí)候會產(chǎn)生一些意外的情況,例如當(dāng)我們這樣寫"12.345-6.A".split(".") 的時(shí)候,我們期待的結(jié)果是得到一個(gè)[12, 345-6, A]數(shù)組。但是Java的split方法竟然返回一個(gè)空數(shù)組!這是應(yīng)為它將一個(gè)正則表達(dá)式作為參數(shù),并根據(jù)表達(dá)式將字符串分割成多個(gè)字符串。這里的點(diǎn)(.)是表示任何字符的正則表達(dá)式。
在Kotlin中不會出現(xiàn)這種令人費(fèi)解的情況,因?yàn)檎齽t表達(dá)式需要一個(gè)Regex類型承載,而不是String。這樣確保了字符串不會被當(dāng)做正則表達(dá)式。

println("12.345-6.A".split("\\.|-".toRegex())) //顯示地創(chuàng)建一個(gè)正則表達(dá)式
//輸出
[12, 345, 6, A ]

這里正則表達(dá)式語法與Java的完全相同,我們匹配一個(gè)點(diǎn)(對它轉(zhuǎn)義表示我們指的時(shí)字面量)或者破折號。
對于一些簡單的情況,就不需要正則表達(dá)式了,Kotlin中的spilt擴(kuò)展函數(shù)的其他重載支持任意數(shù)量的純文本字符串分隔符:

println("12.345-6.A".split(".", "-")) //指定多個(gè)分隔符

等同于上面正則的分割。

正則表達(dá)式和三重引號的字符串

現(xiàn)在有這樣一個(gè)需求:解析文件的完整路徑名稱/Users/hubert/kotlin/chapter.adoc 到對應(yīng)的組件:目錄、文件名、擴(kuò)展名。Kotlin標(biāo)準(zhǔn)庫中包含了一些可以用來獲取在給定分隔符第一次(或最后一次)出現(xiàn)之前(或之后)的子字符串的函數(shù)。

 val path = "/Users/hubert/kotlin/chapter.adoc"
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension")

//輸出
Dir: /Users/hubert/kotlin, name: chapter, ext: adoc

解析字符串在Kotlin中變得更加容易,但如果你仍然想使用正則表達(dá)式,也是沒有問題的:

val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
    val (directory, fileName, extension) = matchResult.destructured
    println("Dir: $directory, name: $fileName, ext: $extension")
}

這里正則表達(dá)式寫在一個(gè)三重引號的字符串中。在這樣的字符串中,不需要對任何字符進(jìn)行轉(zhuǎn)義,包括反斜線,所以可以用\. 而不是\\. 來表示點(diǎn),正如寫一個(gè)普通字符串的字面值。在這個(gè)正則表達(dá)式中:第一段(.+) 表示目錄,/ 表示最后一個(gè)斜線,第二段(.+) 表示文件名,\. 表示最后一個(gè)點(diǎn),第三段(.+) 表示擴(kuò)展名。

多行三重引號的字符串

三重引號字符串的目的,不僅在于避免轉(zhuǎn)義字符,而且使它可以包含任何字符,包括換行符。它提供了一種更簡單的方法,從而可以簡單的把包含換行符的文本嵌入到程序中:

val kotlinLogo = """|//
        .|//
        .|/ \
    """.trimMargin(".")
print(kotlinLogo)

//輸出
|//
|//
|/ \

多行字符串包含三重引號之間的所有字符,包括用于格式化代碼的縮進(jìn)。如果要更好的表示這樣的字符串,可以去掉縮進(jìn)(左邊距)。為此,可以向字符串內(nèi)容添加前綴,標(biāo)記邊距的結(jié)尾,然后調(diào)用trimMargin 來刪除每行中的前綴和前面的空格。在這個(gè)例子中使用了. 來作為前綴。

讓你的代碼更整潔:局部函數(shù)和擴(kuò)展

許多開發(fā)人員認(rèn)為,好代碼的重要標(biāo)準(zhǔn)之一就是減少重復(fù)代碼。Kotlin提供了局部函數(shù)來解決常見的代碼重復(fù)問題。下面的例子中是在將user的信息保存到數(shù)據(jù)庫前,對數(shù)據(jù)進(jìn)行校驗(yàn)的代碼:

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}:empty Name")
    }
    if (user.address.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}:empty Name")
    }
    //保存user到數(shù)據(jù)庫
}

分別對每個(gè)屬性校驗(yàn)的代碼就是重復(fù)的代碼,特別當(dāng)屬性多的時(shí)候就重復(fù)的更多。這種時(shí)候?qū)Ⅱ?yàn)證的代碼放到局部函數(shù)中,可以擺脫重復(fù)同時(shí)保持清晰的代碼結(jié)構(gòu)。局部函數(shù),顧名思義就是定義在函數(shù)中的函數(shù)。我們使用局部函數(shù)來改造上面這個(gè)例子:

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    //聲明一個(gè)局部函數(shù)
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            //局部函數(shù)可以直接訪問外部函數(shù)的參數(shù):user
            throw IllegalArgumentException("Can't save user ${user.id}:empty $fieldName")
        }
    }
    validate(user.name,"Name")
    validate(user.address,"Address")
    //保存user到數(shù)據(jù)庫
}

我們還可以繼續(xù)改進(jìn),將邏輯提取到擴(kuò)展函數(shù)中:

class User(val id: Int, val name: String, val address: String)

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user $id:empty $fieldName")
        }
    }
    validate(name, "Name")//擴(kuò)展函數(shù)直接訪問接收者對象user的屬性
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave()
    //保存user到數(shù)據(jù)庫
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容