Ви навчаєтесь, порівнюючи з тим, що вже знаєте. Нещодавно я постраждав через припущення, що Rust працює як Java щодо вирішення версій транзитивних залежностей. У цьому дописі я хочу порівняти ці дві мови.Ви навчаєтесь, порівнюючи з тим, що вже знаєте. Нещодавно я постраждав через припущення, що Rust працює як Java щодо вирішення версій транзитивних залежностей. У цьому дописі я хочу порівняти ці дві мови.

Транзитивне вирішення версій залежностей у Rust та Java: порівняння двох

2025/09/21 23:00

Ви навчаєтесь, порівнюючи з тим, що вже знаєте. Нещодавно я постраждав від припущення, що Rust працює як Java щодо вирішення версій транзитивних залежностей. У цьому пості я хочу порівняти ці два підходи.

Залежності, транзитивність та вирішення версій

Перш ніж заглиблюватися в особливості кожного стеку, давайте опишемо домен і проблеми, які з ним пов'язані.

\ Під час розробки будь-якого проєкту вище рівня Hello World, ймовірно, ви зіткнетеся з проблемами, з якими інші стикалися раніше. Якщо проблема поширена, ймовірність висока, що хтось був достатньо добрим і відповідальним, щоб упакувати код, який її вирішує, для повторного використання іншими. Тепер ви можете використовувати пакет і зосередитися на вирішенні своєї основної проблеми. Так індустрія будує більшість проєктів сьогодні, навіть якщо це приносить інші проблеми: ви стоїте на плечах гігантів.

\ Мови програмування постачаються з інструментами збірки, які можуть додавати такі пакети до вашого проєкту. Більшість із них називають пакети, які ви додаєте до свого проєкту, залежностями. У свою чергу, залежності проєктів можуть мати власні залежності: останні називаються транзитивними залежностями.

Transitive dependencies

На діаграмі вище C і D є транзитивними залежностями.

\ Транзитивні залежності мають власні проблеми. Найбільша з них виникає, коли транзитивна залежність потрібна з різних шляхів, але в різних версіях. На діаграмі нижче A і B обидва залежать від C, але від різних його версій.

Яку версію C повинен включити інструмент збірки у ваш проєкт? Java і Rust мають різні відповіді. Давайте опишемо їх по черзі.

Вирішення версій транзитивних залежностей у Java

Нагадування: код Java компілюється в байткод, який потім інтерпретується під час виконання (і іноді компілюється в нативний код, але це поза межами нашого поточного простору проблем). Спочатку я опишу вирішення залежностей під час виконання та вирішення залежностей під час збірки.

\ Під час виконання Java Virtual Machine пропонує концепцію classpath. Коли потрібно завантажити клас, середовище виконання шукає через налаштований classpath по порядку. Уявіть наступний клас:

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 і визначаєте classpath у командному рядку, як вище, чи запускаєте JAR, який визначає classpath у своєму маніфесті.

\ Давайте змінимо вищенаведений код на наступний:

public static Main {     public static void main(String[] args) {         var dep = new ch.frankel.Dep();     } } 

\ Оскільки новий код безпосередньо посилається на Dep, новий код вимагає вирішення класу під час компіляції. Вирішення classpath працює так само:

javac -cp ./foo.jar:./bar.jar Main 

\ Компілятор шукає Dep у foo.jar, потім у bar.jar, якщо не знайдено. Вищенаведене - це те, що ви вивчаєте на початку свого шляху вивчення Java.

\ Після цього вашою одиницею роботи є Java Archive, відомий як JAR, замість класу. JAR - це покращений ZIP-архів із внутрішнім маніфестом, який вказує його версію.

\ Тепер уявіть, що ви користувач foo.jar. Розробники foo.jar встановлюють певний classpath під час компіляції, можливо, включаючи інші JAR-файли. Вам знадобиться ця інформація для запуску власної команди. Як розробник бібліотеки передає ці знання користувачам нижче за течією?

\ Спільнота запропонувала кілька ідей для відповіді на це питання: Першою відповіддю, яка прижилася, був Maven. Maven має концепцію Project Object Model, де ви встановлюєте метадані свого проєкту, а також залежності. 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 повертає одну версію дубльованої залежності для включення її в classpath компіляції.

\ Якщо A може працювати з C v2.0 або B з C 1.0, чудово! Якщо ні, вам, ймовірно, доведеться оновити версію A або понизити версію B, щоб вирішена версія C працювала з обома. Це ручний процес, який болісний - запитайте мене, звідки я знаю. Гірше, ви можете виявити, що немає версії C, яка працює з обома A і B. Час замінити A або B.

Вирішення версій транзитивних залежностей у Rust

Rust відрізняється від Java в кількох аспектах, але я думаю, що наступні є найбільш релевантними для нашої дискусії:

  • Rust має однакове дерево залежностей під час компіляції та під час виконання
  • Він надає інструмент збірки з коробки, Cargo
  • Залежності вирішуються з вихідного коду

\ Давайте розглянемо їх по одному.

\ Java компілюється в байткод, потім ви запускаєте останній. Вам потрібно встановити classpath як під час компіляції, так і під час виконання. Компіляція з певним classpath і запуск з іншим може призвести до помилок. Наприклад, уявіть, що ви компілюєте з класом, від якого залежите, але клас відсутній під час виконання. Або, альтернативно, він присутній, але в несумісній версії.

\ На відміну від цього модульного підходу, Rust компілює в унікальний нативний пакет код крейту та кожну залежність. Крім того, Rust надає власний інструмент збірки, таким чином уникаючи необхідності пам'ятати особливості різних інструментів. Я згадував Maven, але інші інструменти збірки, ймовірно, мають різні правила для вирішення версії у вищенаведеному випадку використання.

\ Нарешті, Java вирішує залежності з бінарних файлів: JAR-файлів. Навпаки, Rust вирішує залежності з вихідного коду. Під час збірки Cargo вирішує все дерево залежностей, завантажує всі необхідні вихідні коди та компілює їх у правильному порядку.

\ З урахуванням цього, як Rust вирішує версію залежності C у початковій проблемі? Відповідь може здатися дивною, якщо ви з Java-фону, але Rust включає обидві. Дійсно, на діаграмі вище Rust скомпілює A з C v1.0 і скомпілює B з C v2.0. Проблема вирішена.

Висновок

Мови JVM, і Java зокрема, пропонують як classpath під час компіляції, так і classpath під час виконання. Це дозволяє модульність і повторне використання, але відкриває двері для проблем щодо вирішення classpath. З іншого боку, Rust будує ваш крейт в один самодостатній бінарний файл, будь то бібліотека чи виконуваний файл.

\ Щоб дізнатися більше:

  • Maven - Вступ до механізму залежностей
  • Ефективний Rust - Пункт 25: Керуйте своїм графом залежностей

Спочатку опубліковано на A Java Geek 14 вересня 2025

Відмова від відповідальності: статті, опубліковані на цьому сайті, взяті з відкритих джерел і надаються виключно для інформаційних цілей. Вони не обов'язково відображають погляди MEXC. Всі права залишаються за авторами оригінальних статей. Якщо ви вважаєте, що будь-який контент порушує права третіх осіб, будь ласка, зверніться за адресою service@support.mexc.com для його видалення. MEXC не дає жодних гарантій щодо точності, повноти або своєчасності вмісту і не несе відповідальності за будь-які дії, вчинені на основі наданої інформації. Вміст не є фінансовою, юридичною або іншою професійною порадою і не повинен розглядатися як рекомендація або схвалення з боку MEXC.

Вам також може сподобатися