Kotlin 可以從 Rust 學到什麼
大家好,我是螃蟹哥。
作爲新時代的兩門語言,Rust 有哪些優秀的東西值得 Kotlin 學習呢?本文探討這個問題。
在開始之前,我想重申一下,我這裏不是要在兩種語言之間發起語言戰爭,也不是試圖將一種語言變成另一種語言。我花了很多時間分析我想要討論的特性,並自動排除了對一種語言非常有意義而在另一種語言中很荒謬的特性。例如,在 Rust 中要求垃圾收集是愚蠢的(因爲它的主要主張是對內存分配的非常嚴格的控制),反過來,Kotlin 採用借用檢查器是沒有意義的,因爲 Kotlin 採用收集垃圾。
這篇文章中介紹的功能是我認爲可以被任何一種語言採用而不會危及它們的主要設計理念的功能,歡迎反饋和更正。
01 宏
我一直對語言中的宏有愛恨交加。至少,宏應該完全集成在語言中:
-
編譯器需要了解宏(與 C 和 C++ 中的預處理器不同)。
-
宏需要擁有對靜態類型 AST 的完全訪問權限,並能夠安全地修改此 AST。
Rust 宏滿足了這兩個要求,因此,解鎖了一組非常有趣的功能。
例如,dbg!()
宏:
let a = 2;
let b = 3;
dbg!(a + b);
會打印
[src\main.rs:158] a + b = 5
注意:不僅僅是源文件和行號,還有完整表達式(“ a + b
”)。
在debug_plotter
crate 中可以看到宏強大的另一個很好的例子,它允許你繪製變量:
fn main() {
for a in 0..10 {
let b = (a as f32 / 2.0).sin() * 10.0;
let c = 5 - (a as i32);
debug_plotter::plot!(a, b, c; caption = "My Plot");
}
}
Plot
Kotlin 在這方面並非完全不行,因爲註解和註解處理器的混合提供了這樣的功能,不過與 Rust 中宏和屬性所能做到的相去甚遠。主要區別在於,雖然 Kotlin 的方法只允許合法的 Kotlin 代碼出現在 Kotlin 源文件中,但 Rust 允許任何任意語法作爲宏參數出現,並且由宏生成編譯器會接受的正確的 Rust。
一方面,能夠在 Rust 源文件中編寫任何類型的代碼很好(這就是 React 對 JSX 所做的),另一方面,濫用的可能性很高,人們理所當然地擔心有一天 Rust 源文件看起來與 Rust 代碼完全不同。然而,到目前爲止,我的恐懼從未成爲現實,我遇到的大多數宏都非常節制地使用自定義語法。
宏的另一個非常重要的方面是 Rust IDE 理解它們(好吧,至少 CLion 理解它們,當然可能所有 IDE 都可以),並且它們會在出現問題時立即向你顯示錯誤。
宏用於大量場景,併爲 Rust 提供一些非常簡潔的 DSL 功能(例如,用於支持 SQL、Web、圖形等的庫)。
此外,宏與其他特性可以非常巧妙地集成在一起。
02 預處理的屬性
屬性是 Rust 註釋版本,它們以#
或開頭#!
:
#![crate_type = "lib"]
#[test]
fn test_foo() {}
這裏沒有什麼開創性的東西,但我想討論的是條件編譯 [1]。
在 Rust 中,通過將屬性和宏與cfg
結合來實現條件編譯,它既可用作屬性又可用作宏。
宏版本允許你有條件地編譯語句或表達式:
#[cfg(target_os = "macos")]
fn macos_only() {}
在上面的代碼中,函數macos_only()
只有在操作系統是 macOS 時纔會被編譯。
宏版本cfg()
允許你向條件添加更多邏輯:
let machine_kind = if cfg!(unix) {
"unix"
} else { … }
上面的代碼是一個宏,這意味着它在編譯時被評估。編譯器將完全忽略條件的任何部分。
你可能會想知道 Kotlin 中是否需要這樣的功能,我也問過自己同樣的問題。
Rust 在多個操作系統上編譯爲本地可執行文件,如果你想在多個目標上發佈,這使得這種條件編譯非常必要。Kotlin 沒有這個問題,因爲它生成在 JVM 上運行的操作系統中立的可執行文件。
儘管 Java 和 Kotlin 開發人員已經學會了不用預處理器,因爲 C 預處理器給幾乎每個使用過它的人留下了如此糟糕的印象,但在我的職業生涯中,也有一些情況能夠進行條件編譯,包括或排除源代碼文件,甚至只是語句、表達式或函數,都會派上用場。
不管你在這場辯論中的立場如何,我不得不說我真的很喜歡 Rust 生態系統中兩個非常不同的功能:宏和屬性,能夠協同工作以產生如此有用和多功能的特性。
03 擴展 traits
擴展特徵允許你 “事後” 使結構契合特徵,即使你不擁有這些特徵中的任何一個。最後一點需要強調:結構或特徵是否屬於你未編寫的庫並不重要。你仍然可以使該結構符合該特徵。
例如,如果我們想在類型 u8
上實現函數 last_digit()
:
trait LastDigit {
fn last_digit(&self) -> u8;
}
impl LastDigit for u8 {
fn last_digit(&self) -> u8 {
self % 10
}
}
fn main() {
println!("Last digit for 123: {}", 123.last_digit());
// prints “3”
}
我可能對這個特性有偏見,我是第一個在 2016 年爲 Kotlin 建議類似功能的人(鏈接到討論 [2])。
首先,我發現 Rust 語法優雅和簡約(甚至比 Haskell 更好,可以說,比我爲 Kotlin 提出的更好)。其次,能夠以這種方式擴展特徵在建模問題方面釋放了很多可擴展性和功能,但我不會深入研究這個主題,因爲它會花費太長時間(查找 “type classes” 以瞭解你可以實現的目標)。
這種方法還允許 Rust 模仿 Kotlin 的擴展功能,同時提供一種更通用的機制來擴展函數和類型,但代價是語法稍微冗長一些。
04 cargo
在 Gradle 中,Kotlin 擁有非常強大的構建和包管理器。這兩個工具具有相同的功能表現,允許構建複雜的項目,同時還管理庫下載和依賴項解析。
我認爲cargo
是一個更好的替代品的原因是,它在聲明性語法和命令式方面之間進行了清晰的分離。簡而言之,在聲明性cargo.toml
文件中指定用標準的、通用的構建指令,而臨時的、更具程序性的構建步驟則直接在 Rust 中的一個名爲 build.rs
的文件中編寫,使用 Rust 代碼調用一個相當輕量級的構建 API。
相比之下,Gradle 一團糟。首先是因爲它開始在 Groovy 中被指定,現在它支持 Kotlin 作爲構建語言(並且在它開始多年後,這種轉變仍在進行中),但也因爲這兩者的文檔仍然非常糟糕。
我所說的 “糟糕” 並不是指“缺乏”:有很多文檔,只是…… 糟糕、不堪重負,其中大部分已經過時或已棄用,等等…… 只要你需要一些人跡罕至的東西,就需要從 StackOverflow 複製 / 粘貼數百行。插件系統的定義非常鬆散,基本上讓所有插件都可以訪問 Gradle 內部結構中的任何內容。
顯然,我對這個主題非常固執,因爲我創建了一個受 Gradle 啓發的構建工具,但使用了更現代的語法和插件解析方法(稱爲 Kobalt[3]),但與此無關,我認爲cargo
設法取得了非常好的平衡,靈活的構建 + 依賴管理器工具可以充分覆蓋所有默認配置,而不會隨着項目的增長而變得非常複雜。
05 u8, u16, …
在 Rust 中,數字類型非常簡單:u8
是 8 位無符號整數,i16
是 16 位有符號整數,f32
是 32 位浮點數,等等……
這對我來說是一種清新的空氣。在我開始使用這些類型之前,我從未完全確定我一直對 C、C++、Java 等定義這些類型的方式感到不舒服。每當我需要一個數字時,我都會使用int
或Long
作爲默認值。在 C 中,我有時甚至沒有真正理解 long long
其中的含義。
Rust 迫使我密切關注所有這些類型,然後,每當我嘗試執行可能導致錯誤的強制轉換時,編譯器都會毫不留情地告知我。我真的認爲所有現代語言都應該遵循這個約定。
06 編譯器錯誤信息
並不是說 Kotlin 的錯誤消息很糟糕,但 Rust 確實在多個維度上在這裏樹立了新標準。
編譯器錯誤
你可以從 Rust 編譯器中得到以下內容:
-
帶有箭頭、顏色、問題部分的清晰描述的 ASCII 圖形。
-
簡單的英語和詳細的錯誤消息。
-
關於如何解決問題的建議。
-
相關文檔的鏈接,你可以在其中找到有關該問題的更多信息。
我當然希望未來的語言能夠從中獲得靈感。
07 可移植性
大約 25 年前,當 Java 出現時,JVM 做出了承諾:“一次編寫,隨處運行”(“WORA”)。
雖然這一承諾在早些年站不住腳,但不可否認,WORA 在今天已經成爲現實,並且已經存在了幾十年。JVM 代碼不僅可以編寫一次,到處運行,還可以在任何地方編寫此類代碼,這對開發人員來說是一個重要的生產力提升。你可以在任何 Windows、macOS、Linux 上編寫代碼,並在任何 Windows、macOS 和 Linux 上部署。
令人驚訝的是,即使 Rust 生成本機可執行文件,它也具有這種多功能性。無論你在何種操作系統上編寫代碼,生成大量可執行文件都是微不足道的,而且這些可執行文件是原生的,而且由於 LLVM 令人難以置信的技術成就,它的性能也非常出色。
在 Rust 之前,我已經接受了這樣一個事實:如果我想在多個操作系統上運行,我必須付出在虛擬機上運行的代價,但 Rust 改變了這一點。
Kotlin(以及整個 JVM)也開始吸取教訓,通過 GraalVM 等舉措,但爲 JVM 代碼生成可執行文件仍然充滿限制和約束。
原文鏈接:https://www.beust.com/weblog/2021/11/09/what-kotlin-could-learn-from-rust/
參考資料
[1] 條件編譯: https://doc.rust-lang.org/reference/conditional-compilation.html
[2] 鏈接到討論: https://discuss.kotlinlang.org/t/extension-types-for-kotlin/1390
[3] 稱爲 Kobalt: https://beust.com/kobalt/home/index.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/XcAaDgAxtPFKdB7rhBhF3Q