通过与你已知的事物进行比较来学习。我最近因为假设 Rust 在传递依赖版本解析方面与 Java 工作方式相同而吃了亏。在这篇文章中,我想比较这两者。通过与你已知的事物进行比较来学习。我最近因为假设 Rust 在传递依赖版本解析方面与 Java 工作方式相同而吃了亏。在这篇文章中,我想比较这两者。

Rust 和 Java 中的传递性依赖版本解析:两者比较

2025/09/21 23:00

通过与你已知的事物进行比较来学习。我最近因为假设 Rust 在传递性依赖版本解析方面与 Java 工作方式相同而吃了亏。在这篇文章中,我想比较这两者。

依赖性、传递性和版本解析

在深入探讨每个技术栈的细节之前,让我们先描述一下这个领域及其带来的问题。

\ 当开发任何超过 Hello World 级别的项目时,你很可能会面临他人曾经遇到过的问题。如果这个问题很普遍,很可能有人足够友善和具有公民意识,已经将解决该问题的代码打包,供他人重用。现在,你可以使用这个包并专注于解决你的核心问题。这就是当今行业构建大多数项目的方式,即使它带来了其他问题:你站在巨人的肩膀上。

\ 编程语言都配有可以将这些包添加到你项目中的构建工具。大多数工具将你添加到项目中的包称为依赖项。而项目的依赖项也可以有自己的依赖项:后者被称为传递性依赖项

传递性依赖项

在上图中,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?

\ 文档很清楚:最近的那个。

具有不同版本的相同依赖项的依赖解析

在上图中,到 v1 的路径距离为二,一个到 B,然后一个到 C;同时,到 v2 的路径距离为三,一个到 A,然后一个到 D,最后一个到 C。因此,最短路径指向 v1。

\ 然而,在初始图中,两个 C 版本与根工件的距离相同。文档没有提供答案。如果你对此感兴趣,它取决于 A 和 B 在 POM 中的声明顺序!总之,Maven 返回重复依赖项的单一版本,以将其包含在编译类路径中。

\ 如果 A 可以与 C v2.0 一起工作,或者 B 可以与 C 1.0 一起工作,那太好了!如果不行,你可能需要升级 A 的版本或降级 B 的版本,以便解析的 C 版本可以与两者兼容。这是一个痛苦的手动过程——问问我怎么知道的。更糟糕的是,你可能会发现没有一个 C 版本可以同时与 A 和 B 兼容。是时候替换 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 - 依赖机制介绍
  • 高效 Rust - 第 25 条:管理你的依赖图

最初发布于 A Java Geek,2025 年 9 月 14 日

免责声明: 本网站转载的文章均来源于公开平台,仅供参考。这些文章不代表 MEXC 的观点或意见。所有版权归原作者所有。如果您认为任何转载文章侵犯了第三方权利,请联系 [email protected] 以便将其删除。MEXC 不对转载文章的及时性、准确性或完整性作出任何陈述或保证,并且不对基于此类内容所采取的任何行动或决定承担责任。转载材料仅供参考,不构成任何商业、金融、法律和/或税务决策的建议、认可或依据。

您可能也会喜欢

人们为何留下、离开或在工作中走神的心理学

人们为何留下、离开或在工作中走神的心理学

这篇文章《人们为何留下、离开或在工作中走神的心理学》发表在BitcoinEthereumNews.com上。人们为何留下、离开或在工作中走神的心理学 getty 领导者花费大量时间思考为什么有些员工保持忠诚,为什么其他人迅速离开,以及为什么这么多人在走出门之前就已经长期处于脱离状态。人们常常认为员工离开的主要原因是薪酬或晋升。这些确实重要,但它们只是表面现象。更深层次的解释基于心理学。员工根据他们的感受、恐惧、假设以及日常经历是否强化意义感和连接感来做决定。这正是领导者忽视的部分。线索总是存在,但它们常常隐藏在看似微小和常规的日常互动中。日常工作的心理学揭示了人们为什么留下?getty 日常工作的心理学揭示了人们为什么留下?人们留在那些让他们感到被理解的工作场所。任何组织都可以提供灵活性或更好的福利,但这些并不是让人们长期保持忠诚的因素。当员工相信自己的声音很重要,当支持感觉稳定而非控制性,以及当他们的管理者对他们如何体验工作日表现出真诚兴趣时,员工会选择留下。微软的萨提亚·纳德拉经常谈论这一点。他强调,当领导者以学习心态而非回答心态来进行对话时,人们的表现会达到更高水平。这种转变改变了关系的基调。当员工感受到领导者的好奇心时,它会降低防御性,建立信任,并加强承诺。好奇也表示兴趣,而兴趣则表示价值。当人们感到被重视时,他们会留下。当他们没有这种感觉时,他们开始探索其他选择。这就是为什么日常互动的基调如此重要。一个让人感到被忽视的瞬间可能会超过数月的积极意图...
分享
BitcoinEthereumNews2025/12/07 07:45