Kotlin DSL、操作符、中綴表達式 Infix | DSL 詳解 | DSL 設計與應用

Kotlin DSL、操作符、中綴表達式 Infix | DSL 詳解 | DSL 設計與應用

Overview of Content

本文將深入探討 Kotlin 語言的關鍵特性,包括運算符重載、中綴表達式以及 DSL 的設計和應用

首先,我們將透過一元和二元操作符的範例,深入介紹運算符重載的概念與實踐

接著,我們將深入研究中綴表達式的使用方式,提供實用的範例以展示其優雅的語法

最後,我們將深入探討 DSL 的說明和使用,區分內部和外部 DSL,並深入研究帶接收者的 Lambda 在 Kotlin 中的語法糖應用,包括 with、apply 的使用方式。此外,我們將討論如何自訂 DSL 並與操作函數配合,最終深入探討 DSL 與中綴表達式的結合,呈現 Kotlin 語言豐富的特性和優雅的語法設計

閱讀本文,將有助於更全面地理解 Kotlin 語言的強大功能和實際應用場景。

以下參考,Kotlin 進階實戰,如有引用參考本文章請詳註出處,感謝 😀


Kotlin 運算符重載

Java 中無法運算符重載,但 Kotlin 如同 C++ 一樣可以運算符重載,不過 Kotlin 的運算符是指定函數名(不是真正的符號)

以下會舉幾個操作符重載的例子、使用,更多的符號 Mapping 函數名,請參考官方 Kotlin 操作符重載 說明

operator 重載範例:一元、二元操作符

● Kotlin 透過 operator 關鍵字來做到操作符重載,operator 修飾 特定函數名 的函數

A. 一元操作符

表達式Kotlin 重寫
+aa.unaryPlus()
+aa.unaryMinus()
!aa.not()
a++a.inc()
a–a.dec()

B. 二元操作符

表達式Kotlin 重寫
a + ba.plus(b)
a - ba.minus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b)、 a.mod(b) (已棄用)
a..ba.rangeTo(b)

● 完整的操作符請看 Kotlin 官方介紹

● 操作符複寫範例:覆寫 plus(對應 + 號), contains(對應 in 號) 操作符


class Balance(var money : Int) {
    // 加法
    operator fun plus(balance: Balance) : Balance {
        println("money: ${this.money}, other balance: ${balance.money}")
        return Balance(balance.money + this.money)      // 可創建新對象返回
    }
    // 操作函數也可以重載
    operator fun plus(value : Int) : Balance {
        println("money: ${this.money}, value: $value")
        this.money += value
        println("this money: ${this.money}")
        return this         // 可直接返回 this
    }

    operator fun contains(balance: Balance) : Boolean {
        println("contains money: ${this.money}, value: ${balance.money}")
        return this.money == balance.money
    }
}

operator fun String.times(n : Int) : String {
    val builder = StringBuffer()
    repeat(n) {
        builder.append(this)
    }
    return builder.toString()
}



fun main() {
    val p1 = Balance(100)
    val p2 = Balance(30)
    val p3 = Balance(30)

    val p4 =  p1 + p2 + p3

    println("res = ${p4.money}")

    println("\nres = ${(p4 + 100).money}")

    if(p2 in p3) {
        println("\np2 in p3")
    } else {
        println("\np2 not in p3")
    }

//    val a : Long = 100L
//    println("res = ${(p4 + a).money}")      // Int 不會自動匹配 Long (C++ 可以)

    val t = "123" * 3

    println("\nTest: $t")
}

● 上面註解中有說到 Int 不會自動匹配 Long 類型,這同時也說明了 Kotlin 是一門「強類型」語言


Kotlin 中綴表達式 infix

操作符以中綴(infix)形式處於兩個待操作數中間!通常可以用在更人性化的表達方式(想想… 就跟你在寫 SQL 時一樣)

中綴表達式可以表達成類似文本閱讀的效果,它可以省略一般在寫程式時的 .() 符號

● 目前在 Kotlin 中要使用中綴表達式要符合幾個條件,條件如下…

A. 函數需使用 infix 關鍵字

B. 只能接收 一個參數

C. 不接受 vararg 參數

D. 參數不可有默認值

E. 必須定義在「類的方法」中,拓展函數也可以(不能定義在頂層方法)

infix 使用範例

● 依照 Kotlin 對中綴函數的定義條件寫以下範例


// 定義在 Collection (接口) 中
infix fun <T> Collection<T>.has(element: T) = contains(element)

// 定義在 A 類中
infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)

// 不可定義在頂層函數!
/*infix*/ fun <T> test(value : T) = println("$value")

// 只可以定義一個參數
infix fun <T> String.test(value : T/*, i : Int*/) = println("$this: $value")

fun main() {
    val list = listOf(1, 2, 3, 4, 5)

    if (list has 1) {           // 可省略 `.`、`()` 符號,更像英文的表達
        println("Container 1")

    } else {
        println("Not container 1")

    }

    val map = mapOf<Int, String>(1 to "A", 2 to "B", 3 to "C")
    println("map[1]: ${map[1]}")

    val map2 = mapOf<Int, String>(1 with "A", 2 with "B", 3 with "C")
    println("map2[2]: ${map2[2]}")

    "123".test("Hello")
}


DSL 說明、使用

Domain-Specific Language, DSL 是指 特定領域語言,是為了要簡化程序並方便理解,讓非該領域的人員也可以描述的語言

在 Android 中最常見的 DSL 語言就是編寫 Gradle 用的 Groovy

DSL 分類:內部、外部 DSL

DSL 一般分為兩種

A. 外部 DSL:不同於應用系統,它是描述語言的語言通常有「特殊符號」、「格式」(也就是特殊文本);應用程式通常會透過「符號」與「格式」來解析外部 DSL 語言,在轉為城市內部可用的語言!

eg. 正則表達式,SQL,AWK... 等等

B. 內部 DSL:通用語言的 特定語法它是合法程式(自身就屬於程式);但它具有特定風格,專注處理小領域的問題~

● 內部 DSL 特別注重「上下文的概念」,每個 DSL 都會帶入不同的上下文!

「上下文」可理解為「環境」

● 接下來討論的都是 內部 DSL,它通過 Kotlin 來達到 DSL 效果!我們將會討論的、實現的案例如下

帶接收者的 Lambda

運算符重載

中綴表達式

帶接收者的 Lambda:研究 Kotlin 語法糖 withapply

什麼是帶接收者的 Lambda?

帶接收者的意思就是 函數內部帶有拓展類的 this 對象,可以直接調用該對象內的成員;也就是說這種 DSL 會帶有「接收者的上下文環境」!


## A 是接收者類型
## B 是參數類型
## C 是返回類型

A.(B)->C        

以下範例,使用拓展函數創建一個 Int 類型的 DSL,也就是說該 DSL 的上下文環境就是 Int!(this 代表了 Int)


fun main() {
    // Normal lambda
    val sum1 : (Int, Int) -> Int = {
            x : Int, y : Int -> x + y
    }

    // Int DSL
    val sum2 : Int.(Int) -> Int = {
        // this 是當前的數值!
        // it 是傳入的數值!
        this + it
    }

    println(sum1.invoke(1, 2))

    println(1.sum2(2))
}

上面 DSL 的寫法 this 代表了 1,it 代表了 2,最終合計就是 3

Kotlin 自帶的接收者 Lambda 語法糖

函數特色
with帶新返回值的接收者 Lambda
apply無返回接收者 Lambda

A. Kotlin 語法糖:with 原型

with 會將第一個參數作為上下文,當作第二個參數的上下文!


@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

範例:with 使用範例如下,將第一個參數設定為 AccountInfo,接著第二個 Lambda 參數就會用 AccountInfo 作為上下文


class AccountInfo {
    var name : String? = null
    var address : String? = null
    override fun toString(): String =
        "name: $name, address: $address"
}


fun main() {

    val msg = with(AccountInfo()) {
        name = "Alien"
        address = "Earth-Taiwan"

        toString()        // 返回 String
    }

    println(msg)

}

B. Kotlin 語法糖:apply 原型

apply 是配合泛型、拓展函數、Lambda 做出的(我們先專注在 DSL 的部分),DSL 會帶入泛型 <T> 的上下文,也就是 將呼叫者作為上下文帶入 Lambda 中


@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

範例:apply 使用範例如下,將 AccountInfo 作為接收者,帶入 Lambda 作為上下文


class AccountInfo {
    var name : String? = null
    var address : String? = null

    override fun toString(): String =
        "name: $name, address: $address"
}


fun main() {

    val info = AccountInfo().apply {
        name = "Alien"
        address = "Earth-Taiwan"
    }

    println(info)

}

帶接收者的 Lambda:自訂 DSL

A. 基礎類(上下文 Class 類)

之後會透過 DSL 帶入這兩個類的上下文,讓使用者來設定其內部的成員屬性!


class ClassInfo {
    // 成員屬性
    var className: String? = null
    var studentCount: Int = 0
    var teacherInfo: TeacherInfo? = null

    override fun toString(): String {
        return "Class name: $className, studentCount: $studentCount\n$teacherInfo"
    }
}

class TeacherInfo {
    // 成員屬性
    var name: String? = null
    var age: Int? = null

    override fun toString(): String = "Teacher Name: $name, age: $age"
}

B. ClassWrapper 類

該類的職責是包裝 TeacherInfoClassInfo 類… 目的是 1. 對外提供給 TeacherInfo 類物件的實例,之後提供給呼叫者來設定 TeacherInfo 的內部成員;2. 提供與 ClassInfo 相同的成員,目的是不讓使用者直接設定 ClassInfo 類的成員


// 對外提供(暴露)
class ClassWrapper {
    // 建構一個預設物件,這個物件會提供給外部使用者
    private val teacherInfo = TeacherInfo()

    // 提供與 `ClassInfo` 相同的成員
    var className: String? = null
    var studentCount: Int = 0

    // 匿名 Lambda
    fun teacherInfoSetup(init: TeacherInfo.() -> Unit) : TeacherInfo {
        teacherInfo.init()
        return teacherInfo
    }

    internal fun getTeacherInfo() = teacherInfo
}

看不懂 teacherInfoSetup 函數中的 teacherInfo.init()

其實 init 參數就是接收一個 TeacherInfo 物件的匿名拓展 Lambda 函數,我們也可以將其寫的更清晰好懂一點(如下)


fun teacherInfoSetup(init: TeacherInfo.() -> Unit) : TeacherInfo {
    // teacherInfo.init()
    init.invoke(teacherInfo)    // 同上,一樣的效果
    return teacherInfo
}

C. 頂層函數 DslWithClassInfo:透過頂層函數,配合 Kotlin 的擴展函數寫法,就可以完成 DSL 的設置

這個函數的目的就是 替使用者創建各種上下文環境(像是 ClassWrapperClassInfo


// DSL 使用 Wrapper

fun DslWithClassInfo(init: ClassWrapper.() -> Unit) : ClassInfo {
    val wrapper = ClassWrapper()
    // 創建出 ClassWrapper 後,就可以執行拓展函數 `init`(如果不清楚,請看上一個小姐的說明)
    wrapper.init()

    val classInfo = ClassInfo()
    classInfo.className = wrapper.className
    classInfo.studentCount = wrapper.studentCount
    classInfo.teacherInfo = wrapper.getTeacherInfo()

    return classInfo
}

● 使用:最終達到 DSL 效果(如同 Android Gradle 使用);如果好好利用這個特性的話可以加強「物理高內聚」的特性!


fun main() {

    val classInfo = DslWithClassInfo {    // 上下文帶入的 this 是 ClassWrapper
        className = "Apple"
        studentCount = 20

        // 物理高內聚
        teacherInfoSetup {        // 上下文帶入的 this 是 TeacherInfo
            name = "Alien"
            age = 2000
        }
    }

    println(classInfo)
}

reference link

DSL 配合操作函數

● 要創造 DSL 相似的效果,就要覆寫相對應的操作符,一般是寫 invoke 函數,再加上 Lambda 拓展函數… 範例如下

Kotlin 的符號有對應的函數名稱,像是:invoke 代表了 () 操作符

A. 頂層函數 + Lambda 拓展函數,來創建 String DSL 環境


operator fun String.invoke(fn: String.() -> Unit) {
    fn(this)
}

fun main() {
    "Hello Dsl" {
        println(this)
    }
}

B. 頂層函數 + Lambda 拓展函數,來創建 指定類的 DSL 環境


class Dependency {
    fun implementation(lib: String) {
        println("lib: $lib")
    }

    operator fun invoke(action: Dependency.() -> Unit) {
        action()
    }
}


fun main() {
    val dependency = Dependency()

    dependency() {
        implementation("Test_1111")
        implementation("Test_2222")
        implementation("Test_3333")
    }
}

DSL配合:中綴表達式

● Kotlin 使用 infix 關鍵字 後就可以使用中綴表達式;以下透過透過中綴表達式加上拓展函數,來創建 DSL 的效果,讓趨近於自然語言的表達方式!


infix fun Int.isBigThan(value : Int) : Boolean {
    return this > value
}

infix fun Int.isSmallThan(value : Int) : Boolean {
    return this < value
}
fun main() {
    println("1 isBigThan 2: ${1 isBigThan 2}")
    println("1 isSmallThan 2: ${1 isSmallThan 2}")
}


更多的 Kotlin 語言相關文章

在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容!

Kotlin 特性、特點

Kotlin 特性、特點:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用

Kotlin 進階:協程、響應式、異步

Kotlin 進階:協程、響應式、異步:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響