---
# System prepended metadata

title: Kotlni Infix 中綴表達 & Operator & Kotlin DSL
tags: [Kotlin]

---

---
title: 'Kotlni Infix 中綴表達 & Operator & Kotlin DSL'
disqus: kyleAlien
---

Kotlni Infix 中綴表達 & Operator & Kotlin DSL
===

## Overview of Content


以下參考，Kotlin 進階實戰

如有引用參考本文章請詳註出處，感謝 :smile:

:::success
* 如果喜歡讀更好看一點的網頁版本，可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
    
    本篇文章對應的是 [**Kotlin DSL、操作符、中綴表達式 Infix | DSL 詳解 | DSL 設計與應用**](https://devtechascendancy.com/kotlin-dsl-operators-infix-explained/)
:::

[TOC]

## 運算符重載

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

> 以下會舉幾個操作符重載的例子、使用，更多的符號 Mapping 函數名，請參考官方 [**Kotlin 操作符重載**](https://book.kotlincn.net/text/operator-overloading.html) 說明

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

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

    1. **一元操作符**

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


    2. **二元操作符**

        | 表達式 | Kotlin 重寫 |
        | - | - |
        | a + b | a.plus(b) |
        | a - b | a.minus(b) |
        | a * b | a.times(b) |
        | a / b | a.div(b) |
        | a % b | a.rem(b)、 a.mod(b) （已棄用） |
        | a..b | a.rangeTo(b) |
        
        :::success
        * 完整的操作符請看 [**Kotlin 官方介紹**](https://www.kotlincn.net/docs/reference/operator-overloading.html)
        :::


* 操作符複寫範例：覆寫 `plus`(對應 `+` 號), `contains`(對應 `in` 號) 操作符

    ```kotlin=
    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")
    }
    ```
    
    :::warning
    * 上面註解中有說到 Int 不會自動匹配 Long 類型，這同時也說明了 Kotlin 是一門「強類型」語言
    :::
    
    > ![](https://i.imgur.com/vrZWqxB.png)

## 中綴表達式 infix

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

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

* 目前在 Kotlin 中要使用中綴表達式要符合幾個條件，條件如下…
    
    1. 函數需使用 `infix` 關鍵字
    2. 只能接收 **一個參數**
    3. 不接受 `vararg` 參數
    4. 參數不可有默認值
    5. 必須定義在「類的方法」中，拓展函數也可以（不能定義在頂層方法）
	
### infix 使用範例

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

    ```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")
    }
    ```
    > ![](https://i.imgur.com/EkbwbDh.png)

	
    
    
    
## DSL 概述

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

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

### DSL 分類：內部、外部 DSL

* **DSL 一般分為兩種**

    1. **外部 DSL**：不同於應用系統，**它是描述語言的語言**，**通常有「特殊符號」、「格式」**（也就是特殊文本）；應用程式通常會透過「符號」與「格式」來解析外部 DSL 語言，在轉為城市內部可用的語言！
        
        > eg. 正則表達式，SQL，AWK... 等等

    2. **內部 DSL**：通用語言的 **特定語法**，**它是合法程式**（自身就屬於程式）；但它具有特定風格，專注處理小領域的問題～
        
        :::success
        * 內部 DSL 特別注重「上下文的概念」，每個 DSL 都會帶入不同的上下文！
            
            **「上下文」可理解為「環境」**
        :::
        
        :::info
        * 接下來討論的都是 **內部 DSL**，它通過 Kotlin 來達到 DSL 效果！我們將會討論的、實現的案例如下

            * **帶接收者的 Lambda**

            * **運算符重載**
            
            * **中綴表達式**
            
        :::

### 帶接收者的 Lambda：研究 Kotlin 語法糖 `with`、`apply`

* 什麼是帶接收者的 Lambda？ 
    
    帶接收者的意思就是 **函數內部帶有拓展類的 `this` 對象，可以直接調用該對象內的成員**；也就是說這種 DSL 會帶有「**接收者的上下文環境**」！

    ```shell=
    ## A 是接收者類型
    ## B 是參數類型
    ## C 是返回類型
    
    A.(B)->C        
    ```
    
    以下範例，使用拓展函數創建一個 Int 類型的 DSL，也就是說該 **DSL 的上下文環境就是 Int**！（`this` 代表了 Int）

    ```kotlin=
    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
    > ![](https://i.imgur.com/hJttVSk.png)

* **Kotlin 自帶的接收者 Lambda 語法糖**

    | 函數 | 特色 |
    | - | - |
    | `with` | 帶新返回值的接收者 Lambda |
    | `apply` | 無返回接收者 Lambda |
    
    1. **Kotlin 語法糖：`with` 原型**
        
        `with` 會將第一個參數作為上下文，當作第二個參數的上下文！
    
        ```kotlin=
        @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` 作為上下文

        ```kotlin=
        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)

        }
        ```
    
    
    2. **Kotlin 語法糖：`apply` 原型**
            
        `apply` 是配合泛型、拓展函數、Lambda 做出的（我們先專注在 DSL 的部分），DSL 會帶入泛型 `<T>` 的上下文，也就是 **將呼叫者作為上下文帶入 Lambda 中**！

        ```kotlin=
        @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 作為上下文

        ```kotlin=
        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


1. **基礎類**（上下文 Class 類）
    
    之後會透過 DSL 帶入這兩個類的上下文，讓使用者來設定其內部的成員屬性！

    ```kotlin=
    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"
    }
    ```

2. **ClassWrapper 類**：

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

    ```kotlin=
    // 對外提供（暴露）
    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
    }
    ```
    
    :::warning
    * **看不懂 `teacherInfoSetup` 函數中的 `teacherInfo.init()`**？
        
        其實 `init` 參數就是接收一個 `TeacherInfo` 物件的匿名拓展 Lambda 函數，我們也可以將其寫的更清晰好懂一點（如下）
        
        ```kotlin=
        fun teacherInfoSetup(init: TeacherInfo.() -> Unit) : TeacherInfo {
            // teacherInfo.init()
            init.invoke(teacherInfo)    // 同上，一樣的效果
            return teacherInfo
        }
        ```
    :::
    

3. **頂層函數 `DslWithClassInfo`**：透過頂層函數，配合 Kotlin 的擴展函數寫法，就可以完成 DSL 的設置
    
    這個函數的目的就是 **替使用者創建各種上下文環境（像是 `ClassWrapper`、`ClassInfo`）**
    
    ```kotlin=
    // 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 使用）；如果好好利用這個特性的話可以加強「物理高內聚」的特性！

    ```kotlin=
    fun main() {

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

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

        println(classInfo)
    }
    ```
    > ![reference link](https://i.imgur.com/TNfwxnj.png)


### DSL 配合操作函數

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

    > Kotlin 的符號有對應的函數名稱，像是：`invoke` 代表了 `()` 操作符


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

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

        fun main() {
            "Hello Dsl" {
                println(this)
            }
        }
        ```
    
        > ![](https://i.imgur.com/W7DHfj1.png)

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

        ```kotlin=
        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")
            }
        }
        ```
        > ![](https://i.imgur.com/nxPCsvZ.png)

### DSL配合：中綴表達式

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

    
    ```kotlin=
    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}")
    }
    ```

    > ![](https://i.imgur.com/AwfwXwI.png)

## 更多的 Kotlin 語言相關文章

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

### Kotlin 語言基礎

* **Kotlin 語言基礎**：想要建立堅實的 Kotlin 基礎？以下這些文章將帶你深入探索 Kotlin 的關鍵基礎和概念，幫你打造更堅固的 Kotlin 語言基礎

    :::info
    * [**Kotlin 函數、類、屬性 | DataClass、Sealed、Object 關鍵字 | Enum、Companion、NPE**](https://devtechascendancy.com/kotlin-functions_oop_dataclass_sealed_object/)
        
    * [**深入探究 Kotlin 與 Java 泛型：擦除、取得泛型類型、型變、投影 | 協變、逆變**](https://devtechascendancy.com/explore-kotlin-java-generics_type_erasure/)

    
    * [**深入 Kotlin 函數特性：Inline、擴展、標準函數全解析 | 提升程式碼效能與可讀性**](https://devtechascendancy.com/kotlin_inline_extensions_standards-func/)

    * [**Kotlin DSL、操作符、中綴表達式 Infix | DSL 詳解 | DSL 設計與應用**](https://devtechascendancy.com/kotlin-dsl-operators-infix-explained/)
    :::

### Kotlin 特性、特點

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

    :::warning
    * [**Kotlin 代理與懶加載機制：使用、lazy 深度解析**](https://devtechascendancy.com/kotlin-delegate_java-proxy_lateinit_lazy/)
    
    * [**Kotlin Lambda 編程 & Bytecode | Array & Collections 集合 | 集合函數式 API**](https://devtechascendancy.com/kotlin-lambda-bytecode-array-collections-functional/)
    
    * [**深入理解 Kotlin：智能推斷與 Contact 規則**](https://devtechascendancy.com/kotlin-smart-inference-contract-rules-guide/)
    :::

### Kotlin 進階：協程、響應式、異步

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

    :::danger

    * [**應用 Kotlin 協程：對比 Thread、創建協程、任務掛起 | Dispatcher、CoroutineContext、CoroutineScope**](https://devtechascendancy.com/applied-kotlin-coroutines-in-depth-guide/)
    
    * [**Kotlin Channel 使用介紹 | Select、Actor | 生產者消費者**](https://devtechascendancy.com/kotlin-channel_select_actor_cs/)

    * [**探索 Kotlin Flow：基本使用、RxJava 對比、背壓機制 | Flow 細節**](https://devtechascendancy.com/kotlin-flow-usage_compare-rx_backpressure/)
    :::



## Appendix & FAQ

:::info
:::

###### tags: `Kotlin`
