Kotlin教程(二)函式

胡奚冰發表於2018-03-21

寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學習Kotlin的同學。系列文章的知識點會以《Kotlin實戰》這本書中順序編寫,在將書中知識點展示出來同時,我也會新增對應的Java程式碼用於對比學習和更好的理解。

Kotlin教程(一)基礎
Kotlin教程(二)函式
Kotlin教程(三)類、物件和介面
Kotlin教程(四)可空性
Kotlin教程(五)型別
Kotlin教程(六)Lambda程式設計
Kotlin教程(七)運算子過載及其他約定
Kotlin教程(八)高階函式
Kotlin教程(九)泛型


在Kotlin中建立集合

上一章我們已經使用setOf 函式建立一個set了。同樣的,我們也可以用類似的方法建立一個list或者map:

val set = setOf(1, 2, 3)
val list = listOf(1, 2, 3)
val map = mapOf(1 to "one", 2 to "two")
複製程式碼

to 並不是一個特殊的結構,而是一個普通函式,在後面會繼續探討它。 有沒有想過這裡建立出來的set、list、map到底是什麼型別的那?可以通過.javaClass 屬性獲取型別,相當於Java中的getClass() 方法:

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

//輸出
class java.util.LinkedHashSet
class java.util.Arrays$ArrayList
class java.util.LinkedHashMap
複製程式碼

可以看到都是標準的Java集合類,Kotlin沒有自己專門的集合類,是為了更容易與Java程式碼互動,當從Kotlin中呼叫Java函式的時候,不用轉換它的集合類來匹配Java的類,反之亦然。 儘管Kotlin的集合類和Java的集合類完全一致,但Kotlin還不止於此。舉個例子,可以通過以下方法來獲取一個列表中最後一個元素,或者得到一個數字列表的最大值:

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

//輸出
fourteenth
14
複製程式碼

或許你應該知道last()max() 在Java的集合類中並不存在,這應該是Kotlin自己擴充套件的方法,可以你要知道上面我們列印出來的型別明確是Java中的集合類,但在這裡呼叫方法的物件就是這些集合類,又是怎麼做到讓一個Java中的類呼叫它本身沒有的方法那?在後面我們講到擴充套件函式的時候你就會知道了!

讓函式更好呼叫

現在我們知道了如何建立一個集合,接下來讓我們列印它的內容。Java的集合都有一個預設的toString 實現,但它的何世華的輸出是固定的,而且往往不是你需要的樣子:

val list = listOf(1, 2, 3)
println(list) //觸發toString的呼叫

//輸出
[1, 2, 3]
複製程式碼

假設你需要用分號來分隔每一個元素,然後用括號括起來,而不是採用預設實現。要解決這個問題,Java專案會使用第三方庫,比如Guava和Apache Commons,或者是在這個專案中重寫列印函式。在Kotlin中,它的標準庫中有一個專門的函式來處理這種情況。 但是這裡我們先不借助Kotlin的工具,而是自己寫實現函式:

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()
}
複製程式碼

這個函式是泛型,它可以支援元素為任意型別的集合。讓我們來驗證一下,這個函式是否可行:

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

//輸出
(1;2;3)
複製程式碼

看來是可行的,接下來我們要考慮的是如何修改讓這個函式的呼叫更加簡潔呢?畢竟每次呼叫都要傳入四個引數也是挺麻煩的。

命名引數

我們關注的第一個問題就是函式的可讀性。就以joinToString 來看:

joinToString(list, "", "", "")
複製程式碼

你能看得出這些String都對應什麼引數嗎?可能必須要藉助IDE工具或者檢視函式說明或者函式本身才能知道這些引數的含義。 在Kotlin中,可以做的更優雅:

println(joinToString(list, separator = "", prefix = "", postfix = ""))
複製程式碼

當你呼叫一個Kotlin定義的函式時,可以顯示得標明一些引數的名稱。如果在呼叫一個函式時,指明瞭一個引數的名稱,為了避免混淆,那它之後的所有引數都需要標明名稱。

ps: 當你在Kotlin中呼叫Java定義的函式時,不能採用命名引數。因為把引數名稱存到 .class檔案是Java8以及更高版本的一個可選功能,而Kotlin需要保持和Java6的相容性。

可能到這裡你只是覺得命名引數讓函式便於理解,但是呼叫變得複雜了,我還得多寫引數的名稱!別急,與下面說的預設引數相結合時,你就知道命名引數的好了。

預設引數值

Java的另一個普遍存在問題是:一些類的過載函式實在太多了。這些過載大多是為了向後相容,方便API的使用者,最終導致的結果是重複。 在Kotlin中,可以在宣告函式的時候,指定引數的預設值,這樣可以避免建立過載的函式。讓我們嘗試改進一下前面的joinToString 函式。在大多數情況下,我們可能只會改變分隔符或者改變前字尾,所以我們把這些設定為預設值:

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()
}
複製程式碼

現在在呼叫一下這個函式,可以省略掉有預設值的引數,效果就像在Java中宣告的過載函式一樣。

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

//輸出
1,2,3
1;2;3
複製程式碼

當你使用常規的呼叫語法時,必須按照函式申明中定義的引數順序來給定引數,可以省略的只有排在末尾的引數。如果使用命名引數,可以省略中的一些引數,也可以以你想要的任意 順序只給定你需要的引數:

//打亂了引數順序,並且separator引數使用了預設值
println(joinToString(prefix = "{", collection = list, postfix = "}"))

//輸出
{1,2,3}
複製程式碼

注意,引數的預設值是被編譯到被呼叫的函式中,而不是呼叫的地方。如果你改變了引數預設值並重新編譯這個函式,沒有給引數重新賦值的呼叫者,將會開始使用新的預設值。

Java沒有引數預設值的概念,當你從Java中呼叫Kotlin函式的時候,必須顯示得指定所有引數值。如果需要從Java程式碼中呼叫也能更簡便,可以使用@JvmOverloads註解函式。這個指示編譯器生成Java的過載函式,從最後一個開始省略每個函式。例如joinToString函式,編譯器就會生成如下過載函式: 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) 因此,當你專案同時存在Java和Kotlin時,對有預設引數值的函式習慣性地加上@JvmOverloads註解是個不錯的做法。

消除靜態工具類:頂層函式和屬性

Java作為一門面對物件的語言,需要所有的程式碼都寫作類的函式。但實際上專案中總有一些函式不屬於任何一個類,最終產生了一些類不包含任何狀態或者例項函式,僅僅是作為一堆靜態函式的容器。在JDK中,最明顯的例子應該就是Collections了,還有你的專案中是不是有很多以Util作為字尾的類? 在Kotlin中,根本不需要去建立這些無意義的類,你可以把這些函式直接放到程式碼檔案的頂層,不用從屬於任何類。事實上joinToString函式之前就是直接定義在Join.kt 檔案。

package com.huburt.imagepicker

@JvmOverloads
fun <T> joinToString(...): String {...}
複製程式碼

這會怎樣執行呢?當編譯這個檔案的時候,會生成一些類,因為JVM只能執行類中的程式碼。當你在使用Kotlin的時候,知道這些就夠了。但是如果你需要從Java中來呼叫這些函式,你就必須理解它將怎樣被編譯,來看下編譯後的類是怎樣的:

package com.huburt.imagepicker

public class JoinKt {
	public static String joinToString(...){...}
}
複製程式碼

可以看到Kotlin編譯生成的類的名稱,對應於包含函式的檔名稱,這個檔案中的所有頂層函式編譯為這個類的靜態函式。因此,當從Java呼叫這個函式的時候,和呼叫任何其他靜態函式一樣簡單:

import com.huburt.imagepicker.JoinKt

JoinKt.joinToString(...)
複製程式碼

修改檔案類名

是不是覺得Kt結尾的類使用起來很彆扭,Kotlin提供了方法改變生成類的類名,只需要為這個kt檔案新增@JvmName的註解,將其放到這個檔案的開頭,位於包名的前面:

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

@JvmOverloads
fun <T> joinToString(...): String {...}
複製程式碼

現在就可以用新的類名呼叫這個函式:

import com.huburt.imagepicker.Join

Join.joinToString(...)
複製程式碼

頂層屬性

和函式一樣,屬性也可以放到檔案的頂層。從Java的角度來看就是靜態屬性,沒啥特別的,而且由於沒有了類的存在,這種屬性用到的機會也不多。 需要注意的是頂層函式和其他任意屬性一樣,預設是通過訪問器暴露給Java使用的(也就是通過getter和setter方法)。為了方便使用,如果你想要把一個常量以public static final 的屬性暴露給Java,可以用const 來修飾屬性:

const val TAG = "tag"
複製程式碼

這樣就等同與Java的:

public static final String TAG = "tag"
複製程式碼

給別人的類新增方法:擴充套件函式和屬性

Kotlin的一大特色就是可以平滑的與現有程式碼整合。你可以完全在原有的Java程式碼基礎上開始使用Kotlin。對於原有的Java程式碼可以不修改原始碼的情況下擴充套件功能:擴充套件函式。這一點是我認為Kotlin最強大的地方了。 擴充套件函式非常簡單,它就是一個類的成員函式,不過定義在類的外面。為了方便闡述,讓我們新增一個方法,來計算一個字串的最後一個字元:

package strings

	//String ->接收者型別               //this ->接收者型別
fun String.lastChar(): Char = this.get(this.length - 1)
複製程式碼

你所要做的,就是把你要擴充套件的類或者介面的名稱,放到即將新增的函式前面,這個類的名稱被稱為接收者型別;用來呼叫這個擴充套件函式的那個物件,叫做接收者物件。 接著就可以像呼叫類的普通成員函式一樣去呼叫這個函式了:

println("Kotlin".lastChar())
//輸出
n
複製程式碼

在這個例子中,String就是接收者型別,而“Kotlin”就是接收者物件。 在這個擴充套件函式中,可以像其他成員函式一樣用this,也可以像普通函式一樣省略它:

package strings

fun String.lastChar(): Char = get(length - 1) //省略this呼叫string物件其他函式
複製程式碼

匯入擴充套件函式

對於你定義的擴充套件函式,它不會自動的在整個專案範圍內生效。如果你需要使用它,需要進行匯入,匯入單個函式與匯入類的語法相同:

import strings.lastChar

val c = "Kotlin".lastChar()
複製程式碼

當然也可以用*表示檔案下所有內容:import strings.*

另外還可以使用as 關鍵字來修改匯入的類或則函式的名稱:

import strings.lastChar as last

val c = "Kotlin".last()
複製程式碼

在匯入的時候重新命名可以解決函式名重複的問題。

從Java中呼叫擴充套件函式

實際上,擴充套件函式是靜態函式,它把呼叫物件作為函式的第一個引數。在Java中呼叫擴充套件函式和其他頂層函式一樣,通過.kt檔案生成Java類呼叫靜態的擴充套件函式,把接收者物件傳入第一個引數即可。例如上面提到的lastChar擴充套件函式是定義在StringUtil.kt中,在Java中就可以這樣呼叫:

char c = StringUtilKt.lastChar("Java")
複製程式碼

作為擴充套件函式的工具函式

現在我們可以寫一個joinToString 函式的終極版本了,它和你在Kotlin標準庫中看到的一模一樣:

@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 = "}"))
複製程式碼

將原來的引數Collection,提出來,作為接收者型別編寫的擴充套件函式,使用方法也像是Collection類的成員函式一樣了(當然Java呼叫還是靜態方法,第一個引數傳入Collection物件)。

不可重寫的擴充套件函式

先來看一個重寫的例子:

//Kotlin中class預設是final的,如果需要繼承需要修飾open,函式也相同
open class View {
    open fun click() = println("View clicked")
}

class Button : View() { //繼承
    override fun click() = println("Button clicked")
}
複製程式碼

當你宣告瞭型別為View的變數,那它可以被賦值為Button型別的物件,因為Button是View的一個子類。當你在呼叫這個變數的一般函式,比如click的時候,如果Button複寫了這個函式,name這裡將會呼叫到Button中複寫的函式:

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

//輸出
Button clicked
複製程式碼

但是對於擴充套件函式來說,並不是這樣的。擴充套件函式並不是類的一部分,它是宣告在類之外的。儘管可以給基類和子類都分別定義一個同名的擴充套件函式,當這個函式被呼叫時,它會用到哪一個呢?這裡,它是由該變數的靜態型別所決定的,而不是這個變數的執行時型別。

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!
複製程式碼

當你在呼叫一個型別為View的變數的showOff函式時,對應的擴充套件函式會被呼叫,儘管實際上這個變數現在是一個Button物件。回想一下,擴充套件函式會在Java中編譯為靜態函式,同時接受值將會作為第一個引數。這樣其實2個showOff擴充套件函式就是不同引數的靜態函式,

View view = new Button();
XxKt.showOff(view);  //定義在Xx.kt檔案中
複製程式碼

引數的型別決定了呼叫那個靜態函式,想要呼叫Button的擴充套件函式,則必須先將引數轉成Button型別才行:XxKt.showOff((Button)view);

因此,擴充套件函式也是有侷限性的,擴充套件函式是能擴充套件,即定義新的函式,而不能重寫改變原有函式的實現(本質是一個靜態函式)。如果定了一個類中本身存在成員函式同名的擴充套件函式,Kotlin種呼叫該方法的時候會如何呢?(Java中沒有這個顧慮,呼叫方式不同)

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

fun View.click() = println("擴充套件函式")

val view = View()
view.click()

//輸出
View clicked
複製程式碼

明顯了吧~ 對於有同名成員函式和擴充套件函式時,在Kotlin中呼叫始終執行成員函式的程式碼,擴充套件函式並不起作用,相當於沒有定義。這一點在實際開發中需要特別注意了!

擴充套件屬性

擴充套件屬性提供了一種方法,用於擴充套件類的API,可以用來訪問屬性,用的是屬性語法而不是函式的語法。儘管他們被稱為屬性,但它們可以沒有任何狀態,因為沒有合適的地方來儲存它,不可能給現有的Java物件的例項新增額外的欄位。舉個例子吧:

val String.lastChar: Char
    get() = get(length - 1)
複製程式碼

同樣是獲取字串的最後一個字元,這次是用擴充套件屬性的方式定義。擴充套件屬性也像接收者的一個普通成員屬性一樣,這裡必須定義getter函式,因為沒有支援欄位,因此沒有預設的getter的實現。同理,初始化也不可以:因為沒有地方儲存初始值。 剛剛定義是一個val的擴充套件屬性,也可以定義var屬性:

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value) {
        setCharAt(length - 1, value)
    }
複製程式碼

還記得上一篇文章的自定義訪問器的內容嗎?這裡的定義方式與自定義訪問器一致,val 屬性不可變,因此只需要定義getter,而var 屬性可變,所以getter和setter都需要。 可能不是很好理解擴充套件屬性,或者會和真正的屬性混淆,下面列出了擴充套件屬性轉換成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);
   }
複製程式碼

和擴充套件函式是相同的,僅僅是靜態函式:提供獲取lastChar的功能,這樣的定義方式可以在Kotlin中像使用普通屬性的呼叫方式來使用擴充套件屬性,給你一種這是屬性的感覺,但本質上在Java中就是靜態函式。

處理集合:可變引數、中綴呼叫和庫的支援

擴充套件Java集合的API

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

還記的之前我們使用上面的方式獲取了list的最後一個元素,以及set中的最大值。到這裡你可能已經知道了,last()max() 都是擴充套件函式,自己點進方法驗證一下吧!

可變引數

如果你也看了listOf 函式的定義,你一定看到了這個:

public fun <T> listOf(vararg elements: T): List<T>
複製程式碼

也就是vararg 關鍵字,這讓函式支援任意個數的引數。在Java中同樣的可變引數是在型別後面跟上... ,上面的方法在Java則是:

public <T> List<T> listOf(T... elements)
複製程式碼

但是Kotlin的可變引數相較於Java還是有點區別:當需要傳遞的引數已經包裝在陣列中時,呼叫該函式的語法,在Java中可以按原樣傳遞陣列,而Kotlin則要求你顯示地解包陣列,以便每個陣列元素在函式中能作為單獨的引數來呼叫。從技術的角度來講,這個功能被稱為展開運算子,而使用的時候,不過是在對應的引數前面放一個*

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]
複製程式碼

通過對照可以看到,如果不加* ,其實是把陣列物件當做了集合的元素。加上* 才是將陣列中所有元素新增到集合中。listOf 也可以指定泛型<String> ,你可以嘗試在listOf("c", array) 這裡加泛型,第二個引數array就會提示型別不正確。

Java中沒有展開,我們也可以呼叫Kotlin的listOf函式,該函式宣告在Collections.kt檔案下:

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

//輸出
[a, b]
複製程式碼

Java中可以直接傳入陣列,但是不能同時傳入單個元素和陣列。

鍵值對的處理:中綴呼叫和解構宣告

還記得建立map的方式嗎?

val map = mapOf(1 to "one", 7 to "seven", 52 to "fifty-five")
複製程式碼

之前說過to 並不是一個內建的結構,而是一種特殊的函式呼叫,被稱為中綴呼叫。 在中綴呼叫中,沒有新增額外的分隔符,函式名稱是直接放在目標物件名稱和引數之間的,以下兩種呼叫方式是等價的:

1.to("one")//普通呼叫
1 to "one" //中綴呼叫
複製程式碼

中綴呼叫可以與只有一個引數的函式一起使用,換句話說就是隻要函式只有一個引數,都可以支援在Kotlin中的中綴呼叫,無論是普通的函式還是擴充套件函式。要允許使用中綴符號呼叫函式,需要使用infix 修飾符來標記它。例如to 函式的宣告:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
複製程式碼

to函式會返回一個Pair型別的物件,Pair是Kotlin標準庫中的類,它是用來表示一對元素。我們也可以直接用Pair的內容來初始化兩個變數:

val (number, name) = 1 to "one"
複製程式碼

這個功能稱之為解構宣告,1 to "one" 會返回一個Pair物件,Pair包含一對元素,也就是1和one,接著又定義了變數(number, name) 分別指向Pair中的1和one。 解構宣告特徵不止用於Pair。還可以使用map的key和value內容來初始化兩個變數。並且還適用於迴圈,正如你在使用的withIndex 函式的joinToString 實現中看到的:

for ((index, element) in collection.withIndex()) {
    println("$index, $element")
}
複製程式碼

to 函式是一個擴充套件函式,可以建立一對任何元素,這意味著它是泛型接受者的擴充套件:可以使用1 to "one""one" to 1list to list.size()等寫法。我們來看看mapOf 函式的宣告:

public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>
複製程式碼

listOf 一樣,mapOf 接受可變數量的引數,但這次他們應該是鍵值對。儘管在Kotlin中建立map可能看起來像特殊的解構,而它不過是一個具有簡明語法的常規函式。

字串和正規表示式的處理

Kotlin定義了一系列擴充套件函式,使標準Java字串使用起來更加方便。

分割字串

Java中我們會使用String的split方法分割字串。但有時候會產生一些意外的情況,例如當我們這樣寫"12.345-6.A".split(".") 的時候,我們期待的結果是得到一個[12, 345-6, A]陣列。但是Java的split方法竟然返回一個空陣列!這是應為它將一個正規表示式作為引數,並根據表示式將字串分割成多個字串。這裡的點(.)是表示任何字元的正規表示式。 在Kotlin中不會出現這種令人費解的情況,因為正規表示式需要一個Regex型別承載,而不是String。這樣確保了字串不會被當做正規表示式。

println("12.345-6.A".split("\\.|-".toRegex())) //顯示地建立一個正規表示式
//輸出
[12, 345, 6, A ]
複製程式碼

這裡正規表示式語法與Java的完全相同,我們匹配一個點(對它轉義表示我們指的時字面量)或者破折號。 對於一些簡單的情況,就不需要正規表示式了,Kotlin中的spilt擴充套件函式的其他過載支援任意數量的純文字字串分隔符:

println("12.345-6.A".split(".", "-")) //指定多個分隔符
複製程式碼

等同於上面正則的分割。

正規表示式和三重引號的字串

現在有這樣一個需求:解析檔案的完整路徑名稱/Users/hubert/kotlin/chapter.adoc 到對應的元件:目錄、檔名、副檔名。Kotlin標準庫中包含了一些可以用來獲取在給定分隔符第一次(或最後一次)出現之前(或之後)的子字串的函式。

 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中變得更加容易,但如果你仍然想使用正規表示式,也是沒有問題的:

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

這裡正規表示式寫在一個三重引號的字串中。在這樣的字串中,不需要對任何字元進行轉義,包括反斜線,所以可以用\. 而不是\\. 來表示點,正如寫一個普通字串的字面值。在這個正規表示式中:第一段(.+) 表示目錄,/ 表示最後一個斜線,第二段(.+) 表示檔名,\. 表示最後一個點,第三段(.+) 表示副檔名。

多行三重引號的字串

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

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

//輸出
|//
|//
|/ \
複製程式碼

多行字串包含三重引號之間的所有字元,包括用於格式化程式碼的縮排。如果要更好的表示這樣的字串,可以去掉縮排(左邊距)。為此,可以向字串內容新增字首,標記邊距的結尾,然後呼叫trimMargin 來刪除每行中的字首和前面的空格。在這個例子中使用了. 來作為字首。

讓你的程式碼更整潔:區域性函式和擴充套件

許多開發人員認為,好程式碼的重要標準之一就是減少重複程式碼。Kotlin提供了區域性函式來解決常見的程式碼重複問題。下面的例子中是在將user的資訊儲存到資料庫前,對資料進行校驗的程式碼:

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到資料庫
}
複製程式碼

分別對每個屬性校驗的程式碼就是重複的程式碼,特別當屬性多的時候就重複的更多。這種時候將驗證的程式碼放到區域性函式中,可以擺脫重複同時保持清晰的程式碼結構。區域性函式,顧名思義就是定義在函式中的函式。我們使用區域性函式來改造上面這個例子:

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

fun saveUser(user: User) {
    //宣告一個區域性函式
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            //區域性函式可以直接訪問外部函式的引數:user
            throw IllegalArgumentException("Can't save user ${user.id}:empty $fieldName")
        }
    }
    validate(user.name,"Name")
    validate(user.address,"Address")
    //儲存user到資料庫
}
複製程式碼

我們還可以繼續改進,將邏輯提取到擴充套件函式中:

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")//擴充套件函式直接訪問接收者物件user的屬性
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave()
    //儲存user到資料庫
}
複製程式碼

相關文章