寫在開頭:本人打算開始寫一個(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 1
、list 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ù)庫
}