关注【索引目录】服务号,更多精彩内容等你来探索!
通过与已知知识进行比较,你可以学习新知识。最近,我因为认为 Rust 在传递依赖版本解析方面和 Java 一样好而感到困惑。在这篇文章中,我想比较一下这两者。
依赖关系、传递性和版本解析
在深入研究每个堆栈的细节之前,让我们先描述一下领域及其伴随的问题。
在开发任何高于 Hello World 级别的项目时,你很可能会遇到其他人曾经遇到过的问题。如果这个问题很普遍,那么很可能有人足够善良和热心,将解决问题的代码打包出来,供其他人复用。现在,你可以使用这个包,专注于解决你的核心问题。这就是当今业界构建大多数项目的方式,即使它会带来其他问题:你站在巨人的肩膀上。
语言自带构建工具,可以将此类包添加到项目中。大多数构建工具将您添加到项目中的包称为依赖项。反过来,项目的依赖项可以拥有它们自己的依赖项:后者称为传递依赖项。
在上图中,C 和 D 是传递依赖关系。
传递依赖本身也存在问题。最大的问题是当传递依赖需要来自不同路径、不同版本的依赖时。如下图所示,A 和 B 都依赖于 C,但依赖于 C 的不同版本。
你的项目应该使用哪个版本的 C 语言构建工具?Java 和 Rust 对此有不同的答案。让我们依次进行描述。
Java 传递依赖版本解析
提醒:Java 代码会编译为字节码,然后在运行时进行解释执行(有时会编译为本机代码,但这不在我们当前的问题范围内)。我将首先描述运行时依赖项解析和构建时依赖项解析。
在运行时,JVM提供了类路径的概念。当需要加载一个类时,运行时会按顺序搜索已配置的类路径。想象一下下面的类:
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
Dep编译器会在 中查找foo.jar,bar.jar如果未找到,则在 中查找。以上就是你在 Java 学习之旅开始时所学到的内容。
之后,您的工作单元将不再是类,而是 Java 存档(称为 JAR)。JAR 是一个美化的 ZIP 存档,其内部包含一个清单,用于指定其版本。
现在,假设您是 的用户foo.jar。 的开发人员foo.jar在编译时设置了特定的类路径,其中可能包括其他 JAR 文件。您需要这些信息来运行您自己的命令。库开发人员如何将这些知识传递给下游用户?
社区提出了一些想法来回答这个问题:第一个被采纳的答案是 Maven。Maven 有POM的概念,你可以在其中设置项目的元数据以及依赖项。Maven 可以轻松解决传递依赖关系,因为它们也会发布包含自身依赖项的 POM。因此,Maven 可以追踪每个依赖项的依赖关系,直至叶依赖项。
现在回到问题陈述:Maven 如何解决版本冲突?Maven 会解析哪个 C 依赖版本,1.0 还是 2.0?
文档很清楚:最近的。
在上图中,到v1的路径长度为2,先到B,再到C;而到v2的路径长度为3,先到A,再到D,最后到C,因此最短路径指向v1。
然而,在初始图中,两个 C 版本与根构件的距离相同。文档没有提供答案。如果您感兴趣,这取决于POM 中 A 和 B 的声明顺序!总而言之,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 构建为一个独立的二进制文件,无论是库还是可执行文件。
关注【索引目录】服务号,更多精彩内容等你来探索!

