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`
{"metaMigratedAt":"2023-06-17T19:20:12.911Z","metaMigratedFrom":"YAML","breaks":true,"disqus":"kyleAlien","description":"以下參考,Kotlin 進階實戰","contributors":"[{\"id\":\"b46516e7-7bf1-4a16-8812-94720a65596e\",\"add\":17418,\"del\":3225}]","title":"Kotlni Infix 中綴表達 & Operator & Kotlin DSL"}
Expand menu