1. 程式人生 > >Kotlin學習---函式的定義和呼叫(上)

Kotlin學習---函式的定義和呼叫(上)

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

本章節中會展示Kotlin標準庫中用來處理集合的一些方法。另外還包括幾個相關的語法特性: - vararg 可變引數,用來宣告一個函式將可能有任意數量的引數。 - 中綴表示法,當你呼叫一些 只有一個引數 的函式時,使用它可以讓程式碼更加簡練。 - 結構宣告,用來把一個單獨的組合值展開到多個變數中。

1.1.1 擴充套件Java集合的API

作為本章節的第一個小節,我們先看兩個示例:

>>>  val strings: List<String> = listOf("first", "second"
, "fourteenth") >>> strings.last() //獲取集合中最後一個元素 fourteenth >>> val numbers: Collection<Int> = setOf(1, 14, 2) >>> numbers.max() //獲取集合中最大的元素 14
  • Kotlin中使用的集合都是Java標準庫中的集合,Kotlin只是通過擴充套件函式的方式豐富了Java標準庫中集合的相關操作。顯而易見,例子中的last()函式和max()函式都是通過擴充套件函式的方式實現的。
  • 這兩個例子中的函式還有一個共同點,那就是它們可以被任意數量的引數呼叫。在下一節中我們會看到宣告這些函式的語法。

1.1.2 可變引數:讓函式支援任意數量的引數

上一小節中,我們可以傳入任意引數到 listOf 函式中,我們來看一下在Kotlin庫中這個函式的宣告:

public fun <T> listOf(vararg elements: T): kotlin.collections.List<T> { /* compiled code */ }  // 宣告在Collections.Kt檔案中
  • 我們可以看到在Kotlin中使用了 vararg 修飾符來代表可變引數,而在Java中使用的是
  • Kotlin和Java另一個區別在於,需要傳入的引數已經包裝在陣列中時呼叫的語法。在Java中可以直接把包裝好的陣列傳入被呼叫函式中,在Kotlin中需要你顯式的 解包
    陣列。從技術角度來說這個功能稱為 展開運算子 ,在你使用的時候只不過需要在陣列前加一個 * 號。
fun main(args: Array<String>) {
    val list = listOf("args: ", * args)  //展開運算子展開陣列內容
    println(list)
}

這個例子展示了,通過 展開運算子 可以同時傳入 陣列 和一個 固定的值 。顯然這在Java中時不支援的。 接下來讓我們來看看 map ,我們將簡要討論另一種提高Kotlin函式呼叫的可讀性方法:中綴呼叫。

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

我們先用 mapOf 函式建立一個map集合:

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

在本章開頭的時候說到過 to 這個用法。它在Kotlin中不是什麼內建結構,而是一種特殊的函式呼叫,被稱為 中綴呼叫 。 在中綴呼叫中,沒有新增額外的分隔符,函式名稱就放在目標物件和引數之間。以下兩種呼叫方式是一致的:

1.to("one") //一般To函式的呼叫

1 to "one" //使用中綴符號呼叫to函式

中綴呼叫可以與 只有一個引數的函式 一起使用,無論是普通還是還是擴充套件函式。我們來看一下 to 函式的宣告:

infix fun Any.to(other: Any) = Pair(this, other)
  • 如果要使用中綴符號來呼叫函式,首先這個函式需要 infix 修飾符來標記
  • Pair是Kotlin標準庫中的類,它可以用來表示一對元素。 注意,可以直接用Pair的內容來初始化兩個變數:
val (number, name) = 1 to "one"

解構宣告特徵不止用於 pair 。還可以使用map的key和value內容來初始化變數。 這也適用於迴圈,例如說:

val list2 = listOf(1, 2, 3)
for ((index, value) in list2.withIndex()) {
    println("$index:$value")
}

我們來看一下 mapOf 函式的宣告:

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

像listOf一樣,mapOf接收可變數量的引數,但是這一次它們是鍵值對。 接下來讓我們討論一下擴充套件函式如何簡化 字串正則表示式

1.2 字串和正則表示式的處理

Kotlin的字串和Java的字串完全相同。你可以在Kotlin的程式碼中傳遞任意字串給Java函式,你也可以在Java中使用任何Kotlin標準庫中處理字串的函式,這不需要任何的轉換和包裝。 但是Kotlin在Java的基礎上提供了一系列有用的擴充套件函式,是Java字串使用起來更方便。同時,對於Java中有些費解的函式,Kotlin也添加了一些清晰易用的擴充套件。作為體現API的差異化的第一個例子,讓我們來看一下Kotlin中如何分隔字串。

1.2.1 分割字串

首先看下面的程式碼,看最後返回的值是否跟你想的一樣:

//Java
"12.345-6.A".split(".");

上面這一段是Java中很常見的分隔字串的操作。看到這段程式碼我們期望得到的結果是 [12,345-6,A] 陣列。但是Java中返回的是一個 空陣列!如果大家有寫過類似的程式碼,一定知道這是因為在Java中當我們傳入一個(.)引數到split中時,這個引數被當作了 正則表示式 來處理了,並根據正則表示式來將字串分割。這個點號(.)表示任何字元的正則表示式。 在Kotlin中,它把這個費解的函式隱藏了,作為替換,它提供了一些名為split的,具有不同引數的過載的擴充套件函式。其中就有一個函式是接收正則表示式的,這樣就不會出現Java中的情況了。 我們來看一下Kotlin中怎麼處理已點號(.)或者破折號(-)來分隔字串的:

>>>  println("12.345-6.A".split("\\.|-".toRegex()))  //顯式的建立了一個正則表示式
[12, 345, 6, A]

Kotlin使用與Java完全相同的正則表示式語法。上面程式碼中通過匹配逗號和破折號來分隔字串。通過 toRegex() 擴充套件函式將字串轉換成正則表示式。 對於一些簡單的情況,就不需要使用正則表示式了。Kotlin中的sqlit擴充套件函式的其他過載支援任意數量的純文字字串分隔符:

>>>  println("12.345-6.A".split(".", "-"))  指定多個分隔符
[12, 345, 6, A]

除了傳入字串外,也支援字元入參

println("12.345-6.A".split('.', '-'))

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

讓我們用擴充套件函式和正則表示式這兩種方式來解析檔案的完整路徑名稱到對應的元件中:目錄、檔名和副檔名。首先我們看擴充套件函式的方式:

//substringBeforeLast 用來獲取最後一個匹配到的給定字元的之前的值
//substringAfterLast 用來獲取最後一個匹配到的給定字元的之後的值


fun parsePath(path: String) {
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")

    val fileName = fullName.substringBeforeLast(".")
    val extension = fullName.substringAfterLast(".")

    println("Dir:$directory.name:$fileName,ext:$extension")
}

這兩個擴充套件函式能夠很方便的去解析字串,而不需要使用正則表示式,但是可讀性上會差一點。當然Kotlin的標準庫也支援通過正則表示式來處理這件事情。

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

我們看到在寫正則表示式的時候,寫在了一個 三重引號 的字串中。在這個字串中所有的字元都不需要轉義,包括反斜線,所以可以使用 \. 而不是 \\. 來表示點。下面主要介紹三重引號的作用,關於正則表示式相關的語法就不細說了。 其中在對 directory, filename, extension 變數賦值的時候使用瞭解構宣告。destructured 這個屬性把賦給了相應的變數,這個和我們前面說到的 Pair 初始化兩個變數的語法是相同的。

1.2.3 多重引號的字串

上面章節最後介紹了 三重引號字串 ,這一小節讓我們看看它的其他一些用法。 三重引號字串的目的,不僅在於 避免使用轉義字元,而且使它 可以包含任何字元,包括換行符。另外它還提供了一種簡單的方法,可以簡單的把包含換行符的文字嵌入到程式中。例如:

val kotlinLogo = """| //
                   .|//
                   .| \"""

>>>  println(kotlinLogo.trimMargin("."))

| //
|//
| \

多行字串包含了三重引號之間的所有字元,包括 用於格式化程式碼的縮排。如果要更好的表現這樣的字串,可以去掉縮排(左邊距)。為此可以向字串內容 新增字首標記邊距結尾,然後呼叫 trimMargin 刪除每行中的 字首和前面的空格。在例子中字首就是 “.” 號。 三重引號字串可以包含換行,而不需要專門的字元,比如\n。另一方面,可以不必轉義字元 \ ,所以如果寫一段window風格的路徑“C:\Users\yole\kotlin-book” 可以寫成 “C:\Users\yole\kotlin-book”。 在三重引號中,你也可以使用字串模板。需要注意的是,因為 多行字串不支援轉義序列,如果需要在字串的內容中使用美元符號字面量,必須要使用嵌入式表示式,例如:

val price = """${'$'} 99.9"""

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

本小節讓我們來看看Kotlin是怎麼處理重複程式碼的。先看一段程式碼:

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 Address")
    }

    //儲存user到資料庫
}

>>>  saveUser(User(1,"",""))
java.lang.IllegalArgumentException: Can't save user 1: empty Name

我們可以看到在儲存User之前需要對物件值進行校驗,確保每個值都是有效的,這就導致出現了重複的校驗模板程式碼,明顯這不是一段好程式碼。下面我們通過提取區域性函式來避免重複。

fun saveUser(user: User) {
    fun validate(user: User, value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user${user.id}:empty $fieldName")
        }
    }

    validate(user, user.name, "Name")
    validate(user, user.address, "Address")

    //儲存user到資料庫
}

可以看到我們通過在方法中新增區域性函式的方式,把校驗邏輯封裝了起來,這樣程式碼看起來好了很多,同時擴充套件性也得到了提升。但是還有一點就是我們在校驗方法中傳遞了User物件,這有點難看,放心區域性函式支援訪問外層函式的引數。

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {   //不需要在saveUser的函式中重複user引數了
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user${user.id}:empty $fieldName")  //可以直接訪問user物件
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")

    //儲存user到資料庫
}

我們繼續改進,把驗證邏輯放到擴充套件函式中

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user$id:empty $fieldName")
        }
    }

    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser(user:User){
    user.validateBeforeSave() //呼叫擴充套件函式

    //儲存user到資料庫中
}

同時擴充套件函式也可以被宣告為區域性函式,所以這裡可以進一步將User.validateBeforeSave作為區域性函式放在saveUser中。但是深度巢狀的佈局函式會讓人費解。因此一般不建議使用多層巢狀。 下一章節,讓我們看看類的相關操作。感興趣的小夥伴,記得關注一下哈~

對於文中有疑惑的地方,或者有任何意見和建議的地方都可以評論留言,我會第一時間回覆~與君共勉。