你透過與你已知的事物進行比較來學習。我最近因為假設 Rust 在處理傳遞性依賴版本解析方面與 Java 相同而吃了虧。在這篇文章中,我想比較這兩者。你透過與你已知的事物進行比較來學習。我最近因為假設 Rust 在處理傳遞性依賴版本解析方面與 Java 相同而吃了虧。在這篇文章中,我想比較這兩者。

Rust 和 Java 中的傳遞性依賴版本解析:比較兩者

2025/09/21 23:00

你可以通過比較已知事物來學習。我最近因為假設 Rust 在處理傳遞性依賴版本解析方面與 Java 相同而吃了虧。在這篇文章中,我想比較這兩者。

依賴性、傳遞性和版本解析

在深入探討每個技術棧的細節之前,讓我們先描述這個領域及其相關問題。

\ 當開發任何超過 Hello World 級別的專案時,你很可能會面臨他人曾經遇到過的問題。如果這個問題很普遍,很有可能有人足夠友善和具有公民意識,將解決方案打包成程式碼供他人重複使用。現在,你可以使用這個套件並專注於解決你的核心問題。這就是當今產業構建大多數專案的方式,即使它帶來了其他問題:你站在巨人的肩膀上。

\ 程式語言附帶的構建工具可以將這些套件添加到你的專案中。大多數工具將你添加到專案中的套件稱為依賴項。而專案的依賴項也可以有自己的依賴項:後者被稱為傳遞性依賴項

Transitive dependencies

在上圖中,C 和 D 是傳遞性依賴項。

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

構建工具應該在你的專案中包含哪個版本的 C?Java 和 Rust 有不同的答案。讓我們依次描述它們。

Java 傳遞性依賴版本解析

提醒: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?

\ 文檔很明確:最近的那個。

Dependency resolution with the same dependency in different versions

在上圖中,到 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 傳遞性依賴版本解析

Rust 在幾個方面與 Java 不同,但我認為以下幾點對於我們的討論最為相關:

  • Rust 在編譯時和運行時具有相同的依賴樹
  • 它提供了一個開箱即用的構建工具 Cargo
  • 依賴項是從源碼解析的

\ 讓我們逐一檢視它們。

\ 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 構建為一個獨立的二進制文件,無論是庫還是可執行文件。

\ 進一步了解:

  • Maven - 依賴機制介紹
  • Effective Rust - 第 25 條:管理你的依賴圖

最初發布於 A Java Geek,2025 年 9 月 14 日

免責聲明:本網站轉載的文章均來源於公開平台,僅供參考。這些文章不代表 MEXC 的觀點或意見。所有版權歸原作者所有。如果您認為任何轉載文章侵犯了第三方權利,請聯絡 service@support.mexc.com 以便將其刪除。MEXC 不對轉載文章的及時性、準確性或完整性作出任何陳述或保證,並且不對基於此類內容所採取的任何行動或決定承擔責任。轉載材料僅供參考,不構成任何商業、金融、法律和/或稅務決策的建議、認可或依據。
分享文章