你可以通過比較已知事物來學習。我最近因為假設 Rust 在處理傳遞性依賴版本解析方面與 Java 相同而吃了虧。在這篇文章中,我想比較這兩者。
在深入探討每個技術棧的細節之前,讓我們先描述這個領域及其相關問題。
\ 當開發任何超過 Hello World 級別的專案時,你很可能會面臨他人曾經遇到過的問題。如果這個問題很普遍,很有可能有人足夠友善和具有公民意識,將解決方案打包成程式碼供他人重複使用。現在,你可以使用這個套件並專注於解決你的核心問題。這就是當今產業構建大多數專案的方式,即使它帶來了其他問題:你站在巨人的肩膀上。
\ 程式語言附帶的構建工具可以將這些套件添加到你的專案中。大多數工具將你添加到專案中的套件稱為依賴項。而專案的依賴項也可以有自己的依賴項:後者被稱為傳遞性依賴項。

在上圖中,C 和 D 是傳遞性依賴項。
\ 傳遞性依賴項本身存在問題。最大的問題是當一個傳遞性依賴項從不同路徑被需要,但版本不同時。在下圖中,A 和 B 都依賴於 C,但依賴於不同版本的 C。

構建工具應該在你的專案中包含哪個版本的 C?Java 和 Rust 有不同的答案。讓我們依次描述它們。
提醒:Java 程式碼編譯為位元組碼,然後在運行時被解釋(有時也會編譯為本機程式碼,但這超出了我們當前的問題範圍)。我將首先描述運行時依賴解析和構建時依賴解析。
\ 在運行時,Java 虛擬機提供了類路徑的概念。當需要載入一個類時,運行時會按順序搜尋配置的類路徑。想像以下類:
public static Main { public static void main(String[] args) { Class.forName("ch.frankel.Dep"); } }
\ 讓我們編譯並執行它:
java -cp ./foo.jar:./bar.jar Main
\ 上述命令會首先在 foo.jar 中尋找 ch.frankel.Dep 類。如果找到,它會停止搜尋並載入該類,無論該類是否也存在於 bar.jar 中;如果沒找到,它會進一步在 bar.jar 類中尋找。如果仍未找到,它會失敗並拋出 ClassNotFoundException。
\ Java 的運行時依賴解析機制是有序的,並且具有每個類的粒度。無論你是運行 Java 類並在命令行上定義類路徑(如上所示),還是運行在其清單中定義類路徑的 JAR,這都適用。
\ 讓我們將上述程式碼更改為以下內容:
public static Main { public static void main(String[] args) { var dep = new ch.frankel.Dep(); } }
\ 因為新程式碼直接引用 Dep,新程式碼需要在編譯時進行類解析。類路徑解析的工作方式相同:
javac -cp ./foo.jar:./bar.jar Main
\ 編譯器會在 foo.jar 中尋找 Dep,如果沒找到,則在 bar.jar 中尋找。這是你在 Java 學習旅程開始時學到的內容。
\ 之後,你的工作單位是 Java 存檔,即 JAR,而不是類。JAR 是一個增強版的 ZIP 存檔,帶有指定其版本的內部清單。
\ 現在,假設你是 foo.jar 的使用者。foo.jar 的開發者在編譯時設定了特定的類路徑,可能包括其他 JAR。你需要這些信息來運行自己的命令。庫開發者如何將這些知識傳遞給下游使用者?
\ 社區提出了一些想法來回答這個問題:第一個被採納的回應是 Maven。Maven 有專案物件模型的概念,你可以在其中設定專案的元數據以及依賴項。Maven 可以輕鬆解析傳遞性依賴項,因為它們也發布了自己的 POM,包含自己的依賴項。因此,Maven 可以追蹤每個依賴項的依賴項,直到葉子依賴項。
\ 現在,回到問題陳述:Maven 如何解決版本衝突?Maven 將為 C 解析哪個依賴版本,1.0 還是 2.0?
\ 文檔很明確:最近的那個。

在上圖中,到 v1 的路徑距離為二,一個到 B,然後一個到 C;同時,到 v2 的路徑距離為三,一個到 A,然後一個到 D,最後一個到 C。因此,最短路徑指向 v1。
\ 然而,在初始圖中,兩個 C 版本與根工件的距離相同。文檔沒有提供答案。如果你對此感興趣,它取決於 A 和 B 在 POM 中的聲明順序!總之,Maven 返回重複依賴項的單一版本,以將其包含在編譯類路徑中。
\ 如果 A 可以使用 C v2.0 或 B 可以使用 C 1.0,那很好!如果不行,你可能需要升級 A 的版本或降級 B 的版本,使解析的 C 版本能夠與兩者兼容。這是一個痛苦的手動過程——問我怎麼知道的。更糟的是,你可能會發現沒有能同時與 A 和 B 兼容的 C 版本。是時候替換 A 或 B 了。
Rust 在幾個方面與 Java 不同,但我認為以下幾點對於我們的討論最為相關:
\ 讓我們逐一檢視它們。
\ Java 編譯為位元組碼,然後你運行後者。你需要在編譯時和運行時都設定類路徑。使用特定類路徑編譯並使用不同的類路徑運行可能導致錯誤。例如,想像你使用你依賴的類進行編譯,但該類在運行時不存在。或者,它存在,但版本不兼容。
\ 與這種模塊化方法相反,Rust 將 crate 的程式碼和每個依賴項編譯為一個獨特的本機套件。此外,Rust 也提供了自己的構建工具,從而避免了記住不同工具怪癖的需要。我提到了 Maven,但其他構建工具可能在上述用例中有不同的版本解析規則。
\ 最後,Java 從二進制文件解析依賴項:JAR。相反,Rust 從源碼解析依賴項。在構建時,Cargo 解析整個依賴樹,下載所有需要的源碼,並按正確順序編譯它們。
\ 考慮到這一點,Rust 如何解析初始問題中的 C 依賴版本?如果你來自 Java 背景,答案可能看起來很奇怪,但Rust 兩者都包含。實際上,在上圖中,Rust 將使用 C v1.0 編譯 A,並使用 C v2.0 編譯 B。問題解決了。
JVM 語言,特別是 Java,提供了編譯時類路徑和運行時類路徑。它允許模塊化和重用性,但也為類路徑解析問題打開了大門。另一方面,Rust 將你的 crate 構建為一個獨立的二進制文件,無論是庫還是可執行文件。
\ 進一步了解:
最初發布於 A Java Geek,2025 年 9 月 14 日

![[LIVE] 市場更新:Bitcoin 突破 $115,000,Ethereum 上漲 6%,加密貨幣市場普遍上漲](https://static.coinstats.app/news/source/1716914275457.png)
