
以前寫 C# 時,曾經上過 91 的 C# 進階設計課程。那時候印象很深刻的,是用重構和測試把 LINQ 相關的 API,例如 Where、Select 等這類函式慢慢長出來,而不是停在只會使用 LINQ API
原本每天都在用的 LINQ,自己實作一次之後,才會看到它背後其實串著幾個基礎概念:extension method、delegate、lambda、generic、IEnumerable。API 看起來很短,但它把語法、型別設計和資料流都包在一起。這種方式很適合我,因為它不會只停在「這個 API 怎麼用」,還會逼自己去想「為什麼可以這樣設計」
後來學 Kotlin 時,我一直有類似的感覺。employees.map { it.name } 很像 C# 的 employees.Select(x => x.Name),filter 也很像 Where。可是寫久一點就會發現,兩邊只是長得像,細節不完全一樣
Kotlin 的 Collection API 預設是 eager,真的要 lazy pipeline 要切到 Sequence。Kotlin 的 Extension Function 寫法也跟 C# extension method 不同。再加上 inline、trailing lambda、function type、receiver 這些語法,熟悉 C# Lambda 不代表就真的理解 Kotlin Lambda
所以這個系列想做的事,就是把當年練習 C# LINQ 的方法搬到 Kotlin,自己手刻一組 Kotlin Collection API,從 filter、map、fold 開始,一路碰到 Lambda、Extension Function、泛型、Sequence、Scope Functions、型變和 Contract
這樣做不是為了重寫標準函式庫。標準函式庫已經很好用,也不需要我們自己做一套。重點是藉由重做 API,把那些平常藏在一行 chain call 裡的概念拆開來看
這個系列會用 36 篇文章,從 Lambda 的本質一路寫到 Sequence、Scope Functions、泛型型變和 Contract
主軸很單純:用 Kotlin 實作一組 myFilter、myMap、myFold、myGroupBy 之類的函式。名字前面加 my,避免跟標準函式庫撞名,也提醒自己這是學習用的版本,不是要取代 stdlib
平常我們可能會直接寫
employees
.filter { it.department == "Engineering" }
.map { it.name }
這種寫法很順,但它把很多細節都藏起來了。自己實作一次後,會發現不少函式本體只是 for 迴圈加上一點條件或轉換,重點是函式的簽名
inline 為什麼會影響 Lambda 成本這也是我覺得手刻 Collection API 很適合拿來學 Kotlin Lambda 的原因。它夠小,容易測;但一路寫下去,又會自然碰到 Kotlin 進階語法
這個系列是用 TDD 的方式寫的,不過不會走到 baby step 那麼細。如果每篇都照著「寫一個測試 → 讓它通過 → 重構 → 再寫下一個測試」一輪一輪推進,文章會變得非常長,真正想講的 Kotlin 語法反而會被流程本身蓋過去
所以折衷了一下,每篇先把測試案例集中放在前面,讓你一次看完這個函式該有的行為和邊界條件,接著才是實作的程式碼
這只是文章的呈現順序,不是實際寫程式的順序。想自己練習的話,可以把前面那組測試每個都拆開拆細,一次只寫一個,先讓它紅燈,再讓它變綠,跑起來的感覺跟看文章不太一樣
day 01 專心處理環境,後續文章會沿用同一個專案與測試節奏,不用再回頭處理 build 設定
這篇做三件事
工具上我會改用 Kotlin Toolchain CLI。以前開 Kotlin JVM 專案通常會先碰 Gradle,對這個系列來說有點太重。這次只是要一個乾淨、能 build、能 run、能跑 test 的小專案,用 Kotlin Toolchain 的 jvm-cli template 就夠了
Kotlin Toolchain CLI 的做法,跟我在 Kotlin Toolchain:JetBrains 想為 Kotlin 打造一套更完整的工具生態系 這篇提到的一樣
我這次實測的版本是
kotlin --version
輸出是
Kotlin Toolchain version 0.11.0 (35ef359, 2026-05-20)
Kotlin Toolchain 目前仍在 Alpha 階段,這點要先注意。用它做這個系列的範例專案很適合,因為我們需要的是一個乾淨、簡單、能跑測試的 JVM console project,不是複雜的 production build
Kotlin Toolchain 版本和 Kotlin compiler 版本是兩件事。這個系列的 JVM 底層說明以 Kotlin 2.4.0、預設 Lambda 產生模式 invokedynamic 為基準;Toolchain CLI 則沿用下面實測的 0.11.0
這裡要特別提醒:Toolchain 0.11.0 預設帶的 compiler 不是 2.4.0,是 2.3.x。想確認手上實際跑的是哪一版,可以去看 CLI 快取目錄裡的 stdlib jar 檔名,或直接檢查產出 class 的 @Metadata。要對齊 2.4.0 得自己在 module.yaml 指定 compiler 版本
好消息是本系列用到的底層行為(invokedynamic 產生策略、inline 展開、型別擦除)在 2.3 和 2.4 之間沒有變化,所以就算你手上是 2.3.x,文章裡的結論一樣成立。只有要重跑 bytecode 或 benchmark 時,才需要先確認實際版本,不要只看 CLI 版本
kotlin init 可以互動式選 template,也可以直接指定 template。官方文件把這個選項稱為「JVM console application」;在這次實測的 Toolchain 0.11.0 裡,CLI 使用的 template ID 是 jvm-cli
mkdir KotlinLambdaSample
kotlin init jvm-cli --target-dir=KotlinLambdaSample
cd KotlinLambdaSample
--target-dir指向的目錄要先存在,所以前面的mkdir一定要先跑,不然會有問題
成功後會看到類似這樣的訊息
Extracting template jvm-cli to KotlinLambdaSample...
Project successfully generated
Now you may build your project with ./kotlin build or open this folder in an IDE with the Kotlin Toolchain plugin
注意最後建議的是 ./kotlin build,不是全域的 kotlin build。這個 ./kotlin 是專案內的 wrapper script,概念跟 Gradle Wrapper 很像:專案自己帶著固定入口,新成員 clone 下來後不用先手動裝同一版 CLI
jvm-cli template 產出的結構很小
KotlinLambdaSample/
├── kotlin
├── kotlin.bat
├── module.yaml
├── src/
│ ├── main.kt
│ └── World.kt
└── test/
└── WorldTest.kt
module.yaml 只有一行
product: jvm/app
這表示它是一個 JVM application。對這個系列來說夠用了,後面要加自己的 Kotlin 檔案和測試,都可以直接放在這個專案裡
src/main.kt 預設長這樣
fun main() {
println("Hello, ${World.get()}!")
}
src/World.kt 則是一個簡單的 object
object World {
fun get(): String {
return "World";
}
}
這些只是 template 附的範例,後續可以保留、刪掉,或換成自己的檔案。對我們來說,重點是專案骨架已經有了,而且測試工具也接好了
先跑 build
./kotlin build
實測輸出會看到它分別編譯正式程式碼和測試程式碼
00:01.249 INFO :KotlinLambdaSample:compileJvm Compiling module 'KotlinLambdaSample' for platform 'jvm'...
00:02.911 INFO :KotlinLambdaSample:compileJvmTest Compiling module 'KotlinLambdaSample' for platform 'jvm'...
Build successful
再跑程式
./kotlin run
會印出
Hello, World!
00:00.991 INFO :KotlinLambdaSample:runJvm Process exited with exit code 0
第一行是程式輸出,第二行是 Kotlin Toolchain 的執行資訊。exit code 0 表示程式正常結束
jvm-cli template 會在 test/WorldTest.kt 放兩個測試,其中一個是故意失敗的 shouldFail()
@Test
fun shouldFail() {
assertTrue(false)
}
所以你直接跑
./kotlin test
會看到 shouldFail() 失敗,這不是你環境壞掉,是 template 故意讓你看到測試失敗時的輸出
這個系列不需要保留它,把 shouldFail() 和沒用到的 assertTrue import 刪掉即可。保留 doTest() 後再跑一次
./kotlin test
測試就會通過
00:00.676 INFO :KotlinLambdaSample:compileJvmTest Compiling module 'KotlinLambdaSample' for platform 'jvm'...
00:02.470 INFO :KotlinLambdaSample:testJvm Testing module 'KotlinLambdaSample' for platform 'jvm'...
Started WorldTest
Started doTest()
Passed doTest()
Completed WorldTest
Test run finished after 82 ms
[ 4 containers found ]
[ 0 containers skipped ]
[ 4 containers started ]
[ 0 containers aborted ]
[ 4 containers successful ]
[ 0 containers failed ]
[ 1 tests found ]
[ 0 tests skipped ]
[ 1 tests started ]
[ 0 tests aborted ]
[ 1 tests successful ]
[ 0 tests failed ]
到這裡,我們有一個可以 build、run、test 的 Kotlin JVM 專案。後面的文章就不用再處理環境問題,可以直接把焦點放在 Collection API 和 Lambda
後面大多數範例會圍繞同一組員工資料。把以下內容放進 src/Employee.kt,後續文章的測試就能直接共用
data class Employee(
val id: Int,
val name: String,
val department: String,
val salary: Int,
val age: Int,
)
val employees = listOf(
Employee(1, "Alice", "Engineering", 85_000, 30),
Employee(2, "Bob", "Engineering", 72_000, 25),
Employee(3, "Charlie", "Marketing", 65_000, 35),
Employee(4, "Diana", "Marketing", 78_000, 28),
Employee(5, "Eve", "HR", 60_000, 32),
Employee(6, "Frank", "HR", 55_000, 40),
Employee(7, "Grace", "Engineering", 92_000, 45),
)
固定測試資料有個好處:讀者不用每篇重新理解資料長什麼樣,可以把注意力放在函式本身
環境的部分用 Kotlin Toolchain 的 jvm-cli template 解決了,後面 35 篇都在同一個專案裡加檔案,不用再處理 build 相關的問題
下一篇會進入 Lambda 本質,看看 Kotlin 的 Lambda 編譯後到底變成什麼,FunctionN 介面又扮演什麼角色,順便用 myForEach 把整個系列的 TDD 節奏跑第一次
同步刊登於 Blog
圖片來源:AI 產生