2019年3月11日

Kotlin 宣告與呼叫函數

以下說明這些功能:collections, strings 及 regular expressions 的相關函數。以命名的參數及預設值,定義參數。透過擴增的 functions 及 properties,使用 Java Libraries。以 top-level 及 local function 的方式重組程式碼。

建立 collections

kotlin 的 .javaClass 就等同 java 的 getClass()

//println(set.javaClass)
//class java.util.HashSet
val set = hashSetOf(1, 7, 53)

//println(list.javaClass)
//class java.util.ArrayList
val list = arrayListOf(1, 7, 53)

//println(map.javaClass)
//class java.util.HashMap
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

kotlin 沒有建立自己的 collection classes 而是沿用 java 的 collection classes

// last() 取得最後一個 element
val strings = listOf("first", "second", "fourteenth")
println(strings.last())

// max() 取得最大值
val numbers = setOf(1, 14, 2)
println(numbers.max())
// println 會呼叫 list 的 toString()
val list = listOf(1, 2, 3)
println(list)

joinToString 將 collection 以 prefix, separator, postfix 的格式列印出來

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()
}

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(joinToString(list, "; ", "(", ")"))
}

執行結果

(1; 2; 3)

為了避免每一次都需要填寫四個參數的麻煩,可利用以下方式處理

  • Named Arguments 呼叫 function 時,將參數命名
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
  • Default parameter values 函數的參數提供預設值
fun <T> joinToString(
        collection: Collection<T>,
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
): String

接下來可用以下方式呼叫函數

joinToString(list, ", ", "", "")
joinToString(list)
joinToString(list, "; ")
joinToString(list, prefix = "# ")

top-level functions and properties

取代 static utility clsses

建立 join.kt,放入以下 code

package strings

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()
}

$ kotlinc join.kt 編譯後,會產生 strings/JoinKt.class

所有 top-level function 都會成為該 class 的 static methods

如果加上 @file:JvmName("StringFunctions") 就可改變 class 的名稱,為 strings.StringFunctions

@file:JvmName("StringFunctions")

package strings
fun joinToString(...): String { ... }

top-level properties 轉換到 java 時,一樣會產生 accessor methods, val 變數會產生 getter,var 變數產生 getter/setter。

const val UNIX_LINE_SEPARATOR = "\n" 加上 const,就會轉換為 java 的 public static final String UNIX_LINE_SEPARATOR = "\n";

extension functions and properties,將 method 增加到另一個 class 裡面

因 kotlin 通常會增加到某一個既有的 Java project 裡面,故須要將 kotlin 跟既有的 java code 整合在一起。可使用 extension function,在 class 外面訂製一個新的 function

package strings

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

String 稱為 receiver type,也就是要擴充的 class 名稱,this 稱為 receiver object

以下的例子,String 為 receiver type,"Kotlin" 為 receiver object

>>> println("Kotlin".lastChar())
n

也可以將 this 省略

package strings

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

extension functions 必須要另外 import 才能使用

import strings.lastChar
// import strings.*

val c = "Kotlin".lastChar()

也可以在 import 後,用 as 產生別名,這可以解決 conflict 問題

import strings.lastChar as last
val c = "Kotlin".last()

從 java 呼叫 extension functions

如果剛剛的 code 是放在 StringUtil.kt 裡面

/* Java */
char c = StringUtilKt.lastChar("Java");

以 utility class 作為 extensions

將 joinToString 擴充到 Collection 裡面

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(list.joinToString(separator = "; ",
          prefix = "(", postfix = ")"))
}

改成這樣,就只接受 String 的 collection

fun Collection<String>.join(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
) = joinToString(separator, prefix, postfix)

extension functions 無法被 overriding

在 class 裡面定義的 function 可以被 overriding

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

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

fun main(args: Array<String>) {
    val view: View = Button()
    view.click()
}

執行結果

Button clicked

extension functions 無法被 overriding,因為 extension function 是 static method

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

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

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

fun main(args: Array<String>) {
    val view: View = Button()
    view.showOff()
}

執行結果

I'm a view!

extension properties

extension property 就像是 property 再加上 receiver type,壹定要定義 getter,不能夠定義初始值,在 StringBuilder.lastChar 是定義為 var,因為 StringBuilder 裡面存的資料是會變動的。

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

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

fun main(args: Array<String>) {
    // 使用起來就像是 property
    println("Kotlin".lastChar)
    
    val sb = StringBuilder("Kotlin?")
    sb.lastChar = '!'
    println(sb)
}

其他支援 collections 的功能

Kotlin standard library 其他支援 collection 的功能

  • 關鍵字 vararg: 可定義有任意參數數量的 function
  • infix calls
  • destructuring declarations,可 unpack composite value 為 multiple variables

為什麼 Kotlin 跟 Java 使用相同的 collection class 但是卻有更多功能?原因是 last, max function 都是以 extension function 實作的。

>>> val strings: List<String> = listOf("first", "second", "fourteenth")
>>> strings.last()
fourteenth
>>> val numbers: Collection<Int> = setOf(1, 14, 2)
>>> numbers.max()
14
fun <T> List<T>.last(): T { /* returns the last element */ }
fun Collection<Int>.max(): Int { /* finding a maximum in a collection */ }

  • varargs: 讓 function 可接受不定數量的參數
fun listOf<T>(vararg values: T): List<T> { ... }

直接在參數前面加上 * ,稱為 spread operator

fun main(args: Array<String>) {
    val list = listOf("args: ", *args)
    println(list)
}

  • infix call
// regular way 呼叫函數
1.to("one")

// infix call
1 to "one"

以下是簡化版 infix 的定義方式

infix fun Any.to(other: Any) = Pair(this, other)

Pair 是 kotlin standard library 的 class,代表一組 elements,例如 val (number, name) = 1 to "one" 可將 1 to "one" 轉換為 Pair 再提供給 (number, name) 設定變數

for ((index, element) in collection.withIndex()) {
    println("$index: $element")
}
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>

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

使用 strings 及 regular expressions

  • splitting strings

kotlin 使用跟 java 一樣的 regular expression syntax

fun main(args: Array<String>) {
    println( "12.345-6.A".split("\\.|-".toRegex()) )
    println( "12.345-6.A".split(".", "-") )
    
    // [12, 345, 6, A]
}
  • 將 file full path name 切割為 directory, filename, extension

使用標準函式庫的 substring functions

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

fun main(args: Array<String>) {
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
}

使用 regular expression 的方法:regular expression 以 triple-quoted string 撰寫,這樣就不需要 escape characters,所以直接寫 \. 就可以了

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

fun main(args: Array<String>) {
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
}

  • multiline triple-quoted strings

在 triple-quoted string 裡面包含所有的字元,trimMargin 可去掉 . 前面所有字元

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

fun main(args: Array<String>) {
    println(kotlinLogo.trimMargin("."))
}

結果為

| //
|//
|/ \

用 embedded expression 填寫 $ 字元

>>> val price = """${'$'}99.9"""
>>> println(price)
$99.9

利用 local functions 及 extensions 簡化程式碼

以下程式碼中,name 及 address 欄位檢查部分的程式碼有重複

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

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

建立一個新的 local function: validate,利用該 local function 檢查欄位

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

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

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

因為是 local function,可直接使用 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()) {
            throw IllegalArgumentException(
                "Can't save user ${user.id}: " +
                    "empty $fieldName")
        }
    }

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

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

將 validate 改成 extension function,讓程式碼更簡潔

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")
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave()

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

References

Kotlin in Action

沒有留言:

張貼留言