在Kotlin中創(chuàng)建集合
上一章我們已經(jīng)使用setOf
函數(shù)創(chuàng)建一個set了巍扛。同樣的,我們也可以用類似的方法創(chuàng)建一個list或者map:
val set = setOf(1, 2, 3)
val list = listOf(1, 2, 3)
val map = mapOf(1 to "one", 2 to "two")
to
并不是一個特殊的結(jié)構(gòu)弥臼,而是一個普通函數(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ù)的時候,不用轉(zhuǎn)換它的集合類來匹配Java的類拌牲,反之亦然俱饿。
盡管Kotlin的集合類和Java的集合類完全一致,但Kotlin還不止于此塌忽。舉個例子拍埠,可以通過以下方法來獲取一個列表中最后一個元素,或者得到一個數(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自己擴展的方法枣购,可以你要知道上面我們打印出來的類型明確是Java中的集合類,但在這里調(diào)用方法的對象就是這些集合類擦耀,又是怎么做到讓一個Java中的類調(diào)用它本身沒有的方法那棉圈?在后面我們講到擴展函數(shù)的時候你就會知道了!
讓函數(shù)更好調(diào)用
現(xiàn)在我們知道了如何創(chuàng)建一個集合眷蜓,接下來讓我們打印它的內(nèi)容分瘾。Java的集合都有一個默認(rèn)的toString
實現(xiàn),但它的何世華的輸出是固定的吁系,而且往往不是你需要的樣子:
val list = listOf(1, 2, 3)
println(list) //觸發(fā)toString的調(diào)用
//輸出
[1, 2, 3]
假設(shè)你需要用分號來分隔每一個元素德召,然后用括號括起來,而不是采用默認(rèn)實現(xiàn)汽纤。要解決這個問題上岗,Java項目會使用第三方庫,比如Guava和Apache Commons蕴坪,或者是在這個項目中重寫打印函數(shù)肴掷。在Kotlin中,它的標(biāo)準(zhǔn)庫中有一個專門的函數(shù)來處理這種情況背传。
但是這里我們先不借助Kotlin的工具捆等,而是自己寫實現(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) //不用再第一個元素前添加分隔符
result.append(element)
}
result.append(postfix)
return result.toString()
}
這個函數(shù)是泛型,它可以支持元素為任意類型的集合续室。讓我們來驗證一下,這個函數(shù)是否可行:
val list = listOf(1, 2, 3)
println(joinToString(list, ";", "(", ")"))
//輸出
(1;2;3)
看來是可行的谒养,接下來我們要考慮的是如何修改讓這個函數(shù)的調(diào)用更加簡潔呢挺狰?畢竟每次調(diào)用都要傳入四個參數(shù)也是挺麻煩的明郭。
命名參數(shù)
我們關(guān)注的第一個問題就是函數(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)用一個Kotlin定義的函數(shù)時瞳购,可以顯示得標(biāo)明一些參數(shù)的名稱话侄。如果在調(diào)用一個函數(shù)時,指明了一個參數(shù)的名稱学赛,為了避免混淆年堆,那它之后的所有參數(shù)都需要標(biāo)明名稱。
當(dāng)你在Kotlin中調(diào)用Java定義的函數(shù)時盏浇,不能采用命名參數(shù)变丧。因為把參數(shù)名稱存到 .class文件是Java8以及更高版本的一個可選功能,而Kotlin需要保持和Java6的兼容性绢掰。
可能到這里你只是覺得命名參數(shù)讓函數(shù)便于理解痒蓬,但是調(diào)用變得復(fù)雜了,我還得多寫參數(shù)的名稱滴劲!別急攻晒,與下面說的默認(rèn)參數(shù)相結(jié)合時,你就知道命名參數(shù)的好了班挖。
默認(rèn)參數(shù)值
Java的另一個普遍存在問題是:一些類的重載函數(shù)實在太多了鲁捏。這些重載大多是為了向后兼容,方便API的使用者聪姿,最終導(dǎo)致的結(jié)果是重復(fù)碴萧。
在Kotlin中,可以在聲明函數(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) //不用再第一個元素前添加分隔符
result.append(element)
}
result.append(postfix)
return result.toString()
}
現(xiàn)在在調(diào)用一下這個函數(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ù)順序纤垂,并且separator參數(shù)使用了默認(rèn)值
println(joinToString(prefix = "{", collection = list, postfix = "}"))
//輸出
{1,2,3}
注意,參數(shù)的默認(rèn)值是被編譯到被調(diào)用的函數(shù)中磷账,而不是調(diào)用的地方峭沦。如果你改變了參數(shù)默認(rèn)值并重新編譯這個函數(shù),沒有給參數(shù)重新賦值的調(diào)用者逃糟,將會開始使用新的默認(rèn)值吼鱼。
消除靜態(tài)工具類:頂層函數(shù)和屬性
Java作為一門面對對象的語言,需要所有的代碼都寫作類的函數(shù)绰咽。但實際上項目中總有一些函數(shù)不屬于任何一個類菇肃,最終產(chǎn)生了一些類不包含任何狀態(tài)或者實例函數(shù),僅僅是作為一堆靜態(tài)函數(shù)的容器剃诅。在JDK中巷送,最明顯的例子應(yīng)該就是Collections了,還有你的項目中是不是有很多以Util作為后綴的類矛辕?
在Kotlin中笑跛,根本不需要去創(chuàng)建這些無意義的類,你可以把這些函數(shù)直接放到代碼文件的頂層聊品,不用從屬于任何類飞蹂。事實上joinToString函數(shù)之前就是直接定義在Join.kt 文件。
package com.huburt.imagepicker
@JvmOverloads
fun <T> joinToString(...): String {...}
這會怎樣運行呢翻屈?當(dāng)編譯這個文件的時候陈哑,會生成一些類,因為JVM只能執(zhí)行類中的代碼伸眶。當(dāng)你在使用Kotlin的時候惊窖,知道這些就夠了。但是如果你需要從Java中來調(diào)用這些函數(shù)厘贼,你就必須理解它將怎樣被編譯界酒,來看下編譯后的類是怎樣的:
package com.huburt.imagepicker
public class JoinKt {
public static String joinToString(...){...}
}
可以看到Kotlin編譯生成的類的名稱,對應(yīng)于包含函數(shù)的文件名稱嘴秸,這個文件中的所有頂層函數(shù)編譯為這個類的靜態(tài)函數(shù)毁欣。因此,當(dāng)從Java調(diào)用這個函數(shù)的時候岳掐,和調(diào)用任何其他靜態(tài)函數(shù)一樣簡單:
import com.huburt.imagepicker.JoinKt
JoinKt.joinToString(...)
修改文件類名
是不是覺得Kt結(jié)尾的類使用起來很別扭凭疮,Kotlin提供了方法改變生成類的類名,只需要為這個kt文件添加@JvmName
的注解串述,將其放到這個文件的開頭执解,位于包名的前面:
@file:JvmName("Join") //指定類名
package com.huburt.imagepicker
@JvmOverloads
fun <T> joinToString(...): String {...}
現(xiàn)在就可以用新的類名調(diào)用這個函數(shù):
import com.huburt.imagepicker.Join
Join.joinToString(...)
頂層屬性
和函數(shù)一樣,屬性也可以放到文件的頂層纲酗。從Java的角度來看就是靜態(tài)屬性材鹦,沒啥特別的,而且由于沒有了類的存在逝淹,這種屬性用到的機會也不多。
需要注意的是頂層函數(shù)和其他任意屬性一樣桶唐,默認(rèn)是通過訪問器暴露給Java使用的(也就是通過getter和setter方法)。為了方便使用茉兰,如果你想要把一個常量以public static final
的屬性暴露給Java尤泽,可以用const
來修飾屬性:
const val TAG = "tag"
這樣就等同與Java的:
public static final String TAG = "tag"
給別人的類添加方法:擴展函數(shù)和屬性
Kotlin的一大特色就是可以平滑的與現(xiàn)有代碼集成。你可以完全在原有的Java代碼基礎(chǔ)上開始使用Kotlin规脸。對于原有的Java代碼可以不修改源碼的情況下擴展功能:擴展函數(shù)坯约。這一點是我認(rèn)為Kotlin最強大的地方了。
擴展函數(shù)非常簡單莫鸭,它就是一個類的成員函數(shù)闹丐,不過定義在類的外面。為了方便闡述被因,讓我們添加一個方法卿拴,來計算一個字符串的最后一個字符:
package strings
//String ->接收者類型 //this ->接收者類型
fun String.lastChar(): Char = this.get(this.length - 1)
復(fù)制代碼你所要做的,就是把你要擴展的類或者接口的名稱梨与,放到即將添加的函數(shù)前面堕花,這個類的名稱被稱為接收者類型;用來調(diào)用這個擴展函數(shù)的那個對象粥鞋,叫做接收者對象缘挽。
接著就可以像調(diào)用類的普通成員函數(shù)一樣去調(diào)用這個函數(shù)了:
println("Kotlin".lastChar())
//輸出
n
在這個例子中,String就是接收者類型呻粹,而“Kotlin”就是接收者對象壕曼。
在這個擴展函數(shù)中,可以像其他成員函數(shù)一樣用this等浊,也可以像普通函數(shù)一樣省略它:
package strings
fun String.lastChar(): Char = get(length - 1) //省略this調(diào)用string對象其他函數(shù)
導(dǎo)入擴展函數(shù)
對于你定義的擴展函數(shù)腮郊,它不會自動的在整個項目范圍內(nèi)生效。如果你需要使用它凿掂,需要進(jìn)行導(dǎo)入伴榔,導(dǎo)入單個函數(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ù)名重復(fù)的問題。
從Java中調(diào)用擴展函數(shù)
實際上庄萎,擴展函數(shù)是靜態(tài)函數(shù)踪少,它把調(diào)用對象作為函數(shù)的第一個參數(shù)。在Java中調(diào)用擴展函數(shù)和其他頂層函數(shù)一樣糠涛,通過.kt文件生成Java類調(diào)用靜態(tài)的擴展函數(shù)援奢,把接收者對象傳入第一個參數(shù)即可。例如上面提到的lastChar
擴展函數(shù)是定義在StringUtil.kt
中忍捡,在Java中就可以這樣調(diào)用:
char c = StringUtilKt.lastChar("Java")
作為擴展函數(shù)的工具函數(shù)
現(xiàn)在我們可以寫一個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,提出來具篇,作為接收者類型編寫的擴展函數(shù)纬霞,使用方法也像是Collection類的成員函數(shù)一樣了(當(dāng)然Java調(diào)用還是靜態(tài)方法,第一個參數(shù)傳入Collection對象)驱显。
不可重寫的擴展函數(shù)
先來看一個重寫的例子:
//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類型的對象伏恐,因為Button是View的一個子類。當(dāng)你在調(diào)用這個變量的一般函數(shù)栓霜,比如click的時候翠桦,如果Button復(fù)寫了這個函數(shù),name這里將會調(diào)用到Button中復(fù)寫的函數(shù):
val view: View = Button()
view.click()
//輸出
Button clicked
但是對于擴展函數(shù)來說胳蛮,并不是這樣的销凑。擴展函數(shù)并不是類的一部分,它是聲明在類之外的鹰霍。盡管可以給基類和子類都分別定義一個同名的擴展函數(shù)闻鉴,當(dāng)這個函數(shù)被調(diào)用時,它會用到哪一個呢茂洒?這里孟岛,它是由該變量的靜態(tài)類型所決定的,而不是這個變量的運行時類型督勺。
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)用一個類型為View的變量的showOff函數(shù)時渠羞,對應(yīng)的擴展函數(shù)會被調(diào)用,盡管實際上這個變量現(xiàn)在是一個Button對象智哀〈窝回想一下,擴展函數(shù)會在Java中編譯為靜態(tài)函數(shù)瓷叫,同時接受值將會作為第一個參數(shù)屯吊。這樣其實2個showOff擴展函數(shù)就是不同參數(shù)的靜態(tài)函數(shù),
View view = new Button();
XxKt.showOff(view); //定義在Xx.kt文件中
參數(shù)的類型決定了調(diào)用那個靜態(tài)函數(shù)摹菠,想要調(diào)用Button的擴展函數(shù)盒卸,則必須先將參數(shù)轉(zhuǎn)成Button類型才行:XxKt.showOff((Button)view)
;
因此,擴展函數(shù)也是有局限性的次氨,擴展函數(shù)是能擴展蔽介,即定義新的函數(shù),而不能重寫改變原有函數(shù)的實現(xiàn)(本質(zhì)是一個靜態(tài)函數(shù))。如果定了一個類中本身存在成員函數(shù)同名的擴展函數(shù)虹蓄,Kotlin種調(diào)用該方法的時候會如何呢犀呼?(Java中沒有這個顧慮,調(diào)用方式不同)
open class View {
open fun click() = println("View clicked")
}
fun View.click() = println("擴展函數(shù)")
val view = View()
view.click()
//輸出
View clicked
明顯了吧~ 對于有同名成員函數(shù)和擴展函數(shù)時薇组,在Kotlin中調(diào)用始終執(zhí)行成員函數(shù)的代碼外臂,擴展函數(shù)并不起作用,相當(dāng)于沒有定義体箕。這一點在實際開發(fā)中需要特別注意了专钉!
擴展屬性
擴展屬性提供了一種方法,用于擴展類的API累铅,可以用來訪問屬性,用的是屬性語法而不是函數(shù)的語法站叼。盡管他們被稱為屬性娃兽,但它們可以沒有任何狀態(tài),因為沒有合適的地方來存儲它尽楔,不可能給現(xiàn)有的Java對象的實例添加額外的字段投储。舉個例子吧:
val String.lastChar: Char
get() = get(length - 1)
同樣是獲取字符串的最后一個字符,這次是用擴展屬性的方式定義阔馋。擴展屬性也像接收者的一個普通成員屬性一樣玛荞,這里必須定義getter函數(shù),因為沒有支持字段呕寝,因此沒有默認(rèn)的getter的實現(xiàn)勋眯。同理,初始化也不可以:因為沒有地方存儲初始值下梢。
剛剛定義是一個val
的擴展屬性客蹋,也可以定義var
屬性:
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value) {
setCharAt(length - 1, value)
}
還記得上一篇文章的自定義訪問器的內(nèi)容嗎?這里的定義方式與自定義訪問器一致孽江,val
屬性不可變讶坯,因此只需要定義getter,而var
屬性可變岗屏,所以getter和setter都需要辆琅。
可能不是很好理解擴展屬性,或者會和真正的屬性混淆这刷,下面列出了擴展屬性轉(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);
}
和擴展函數(shù)是相同的崭歧,僅僅是靜態(tài)函數(shù):提供獲取lastChar的功能隅很,這樣的定義方式可以在Kotlin中像使用普通屬性的調(diào)用方式來使用擴展屬性,給你一種這是屬性的感覺,但本質(zhì)上在Java中就是靜態(tài)函數(shù)叔营。
處理集合:可變參數(shù)澡谭、中綴調(diào)用和庫的支持
擴展Java集合的API
val strings = listOf("first", "second", "fourteenth")
println(strings.last())
val numbers = setOf(1, 14, 2)
println(numbers.max())
還記的之前我們使用上面的方式獲取了list的最后一個元素,以及set中的最大值望门。到這里你可能已經(jīng)知道了甫菠,last()
和max()
都是擴展函數(shù),自己點進(jìn)方法驗證一下吧婴谱!
可變參數(shù)
如果你也看了listOf
函數(shù)的定義蟹但,你一定看到了這個:
public fun <T> listOf(vararg elements: T): List<T>
也就是vararg
關(guān)鍵字,這讓函數(shù)支持任意個數(shù)的參數(shù)谭羔。在Java中同樣的可變參數(shù)是在類型后面跟上...
华糖,上面的方法在Java則是:
public <T> List<T> listOf(T... elements)
但是Kotlin的可變參數(shù)相較于Java還是有點區(qū)別:當(dāng)需要傳遞的參數(shù)已經(jīng)包裝在數(shù)組中時,調(diào)用該函數(shù)的語法瘟裸,在Java中可以按原樣傳遞數(shù)組客叉,而Kotlin則要求你顯示地解包數(shù)組,以便每個數(shù)組元素在函數(shù)中能作為單獨的參數(shù)來調(diào)用话告。從技術(shù)的角度來講兼搏,這個功能被稱為展開運算符,而使用的時候沙郭,不過是在對應(yīng)的參數(shù)前面放一個*
:
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ù)組對象當(dāng)做了集合的元素病线。加上*
才是將數(shù)組中所有元素添加到集合中吓著。listOf
也可以指定泛型<String>
,你可以嘗試在listOf("c", array)
這里加泛型氧苍,第二個參數(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ù)組。
鍵值對的處理:中綴調(diào)用和解構(gòu)聲明
還記得創(chuàng)建map的方式嗎赡突?
val map = mapOf(1 to "one", 7 to "seven", 52 to "fifty-five")
之前說過to
并不是一個內(nèi)置的結(jié)構(gòu)对扶,而是一種特殊的函數(shù)調(diào)用,被稱為中綴調(diào)用惭缰。
在中綴調(diào)用中浪南,沒有添加額外的分隔符,函數(shù)名稱是直接放在目標(biāo)對象名稱和參數(shù)之間的漱受,以下兩種調(diào)用方式是等價的:
1.to("one")//普通調(diào)用
1 to "one" //中綴調(diào)用
中綴調(diào)用可以與只有一個參數(shù)的函數(shù)一起使用络凿,換句話說就是只要函數(shù)只有一個參數(shù),都可以支持在Kotlin中的中綴調(diào)用,無論是普通的函數(shù)還是擴展函數(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ù)會返回一個Pair類型的對象怨愤,Pair是Kotlin標(biāo)準(zhǔn)庫中的類派敷,它是用來表示一對元素。我們也可以直接用Pair的內(nèi)容來初始化兩個變量:
val (number, name) = 1 to "one"
這個功能稱之為解構(gòu)聲明撰洗,1 to "one"
會返回一個Pair對象篮愉,Pair包含一對元素,也就是1和one差导,接著又定義了變量(number, name)
分別指向Pair中的1和one试躏。
解構(gòu)聲明特征不止用于Pair。還可以使用map的key和value內(nèi)容來初始化兩個變量设褐。并且還適用于循環(huán)冗酿,正如你在使用的withIndex
函數(shù)的joinToString
實現(xiàn)中看到的:
for ((index, element) in collection.withIndex()) {
println("$index, $element")
}
to
函數(shù)是一個擴展函數(shù),可以創(chuàng)建一對任何元素络断,這意味著它是泛型接受者的擴展:可以使用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),而它不過是一個具有簡明語法的常規(guī)函數(shù)开伏。
字符串和正則表達(dá)式的處理
Kotlin定義了一系列擴展函數(shù)膀跌,使標(biāo)準(zhǔn)Java字符串使用起來更加方便。
分割字符串
Java中我們會使用String的split方法分割字符串固灵。但有時候會產(chǎn)生一些意外的情況捅伤,例如當(dāng)我們這樣寫"12.345-6.A".split(".")
的時候,我們期待的結(jié)果是得到一個[12, 345-6, A]
數(shù)組巫玻。但是Java的split方法竟然返回一個空數(shù)組丛忆!這是應(yīng)為它將一個正則表達(dá)式作為參數(shù),并根據(jù)表達(dá)式將字符串分割成多個字符串仍秤。這里的點(.
)是表示任何字符的正則表達(dá)式熄诡。
在Kotlin中不會出現(xiàn)這種令人費解的情況,因為正則表達(dá)式需要一個Regex類型承載诗力,而不是String凰浮。這樣確保了字符串不會被當(dāng)做正則表達(dá)式。
println("12.345-6.A".split("\\.|-".toRegex())) //顯示地創(chuàng)建一個正則表達(dá)式
//輸出
[12, 345, 6, A ]
這里正則表達(dá)式語法與Java的完全相同,我們匹配一個點(對它轉(zhuǎn)義表示我們指的時字面量)或者破折號袜茧。
對于一些簡單的情況菜拓,就不需要正則表達(dá)式了,Kotlin中的spilt擴展函數(shù)的其他重載支持任意數(shù)量的純文本字符串分隔符:
println("12.345-6.A".split(".", "-")) //指定多個分隔符
等同于上面正則的分割惫周。
正則表達(dá)式和三重引號的字符串
現(xiàn)在有這樣一個需求:解析文件的完整路徑名稱/Users/liuboyu/kotlin/chapter.adoc
到對應(yīng)的組件:目錄尘惧、文件名、擴展名递递。Kotlin標(biāo)準(zhǔn)庫中包含了一些可以用來獲取在給定分隔符第一次(或最后一次)出現(xiàn)之前(或之后)的子字符串的函數(shù)喷橙。
val path = "/Users/liuboyu/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/liuboyu/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á)式寫在一個三重引號的字符串中贰逾。在這樣的字符串中,不需要對任何字符進(jìn)行轉(zhuǎn)義菠秒,包括反斜線疙剑,所以可以用\.
而不是\\.
來表示點,正如寫一個普通字符串的字面值践叠。在這個正則表達(dá)式中:第一段(.+)
表示目錄言缤,/
表示最后一個斜線,第二段(.+)
表示文件名禁灼,\.
表示最后一個點管挟,第三段(.+)
表示擴展名。
多行三重引號的字符串
三重引號字符串的目的弄捕,不僅在于避免轉(zhuǎn)義字符僻孝,而且使它可以包含任何字符,包括換行符守谓。它提供了一種更簡單的方法穿铆,從而可以簡單的把包含換行符的文本嵌入到程序中:
val kotlinLogo = """|//
.|//
.|/ \
""".trimMargin(".")
print(kotlinLogo)
//輸出
|//
|//
|/ \
多行字符串包含三重引號之間的所有字符,包括用于格式化代碼的縮進(jìn)斋荞。如果要更好的表示這樣的字符串荞雏,可以去掉縮進(jìn)(左邊距)。為此譬猫,可以向字符串內(nèi)容添加前綴讯檐,標(biāo)記邊距的結(jié)尾,然后調(diào)用trimMargin
來刪除每行中的前綴和前面的空格染服。在這個例子中使用了.
來作為前綴别洪。
讓你的代碼更整潔:局部函數(shù)和擴展
許多開發(fā)人員認(rèn)為,好代碼的重要標(biāo)準(zhǔn)之一就是減少重復(fù)代碼柳刮。Kotlin提供了局部函數(shù)來解決常見的代碼重復(fù)問題挖垛。下面的例子中是在將user的信息保存到數(shù)據(jù)庫前痒钝,對數(shù)據(jù)進(jì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ù)庫
}
分別對每個屬性校驗的代碼就是重復(fù)的代碼,特別當(dāng)屬性多的時候就重復(fù)的更多痢毒。這種時候?qū)Ⅱ炞C的代碼放到局部函數(shù)中送矩,可以擺脫重復(fù)同時保持清晰的代碼結(jié)構(gòu)。局部函數(shù)哪替,顧名思義就是定義在函數(shù)中的函數(shù)栋荸。我們使用局部函數(shù)來改造上面這個例子:
class User(val id: Int, val name: String, val address: String)
fun saveUser(user: User) {
//聲明一個局部函數(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),將邏輯提取到擴展函數(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")//擴展函數(shù)直接訪問接收者對象user的屬性
validate(address, "Address")
}
fun saveUser(user: User) {
user.validateBeforeSave()
//保存user到數(shù)據(jù)庫
}