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 重寫 |
---|---|
+a | a.unaryPlus() |
+a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a– | a.dec() |
B. 二元操作符
表達式 | 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) |
● 完整的操作符請看 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 語法糖 with
、apply
● 什麼是帶接收者的 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 類:
該類的職責是包裝 TeacherInfo
、ClassInfo
類… 目的是 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 的設置
這個函數的目的就是 替使用者創建各種上下文環境(像是 ClassWrapper
、ClassInfo
)
// 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)
}
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 特性、特點:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用
Kotlin 進階:協程、響應式、異步
● Kotlin 進階:協程、響應式、異步:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章