Шлях для доступу до елементів у дереві модулів

Щоб вказати Rust, де шукати елемент у дереві модулів, ми використовуємо шляхи так само як ми використовуємо шляхи для навігації по файловій системі. Щоб викликати функцію, ми повинні знати її шлях.

Шлях може приймати дві форми:

  • Aбсолютний шлях це повний шлях, що починається в кореневій директорії крейту; для коду від зовнішнього крейту, абсолютний шлях починається з назви крейту, і для коду з поточного ящика починається з рядка crate.
  • Відносний шлях починається у поточному модулі і використовує self, super чи ідентифікатор поточного модуля.

І абсолютні, і відносні шляхи складаються з одного чи кількох ідентифікаторів, розділених подвійною двокрапкою (::).

Повернімося до Блоку коду 7-1. Скажімо, ми хочемо викликати функцію add_to_waitlist. Це те саме, що й запитати: який шлях до функції add_to_waitlist? Блок коду 7-3 містить Блок коду 7-1, але деякі з модулів та функцій прибрані.

Ми покажемо два способи викликати функцію add_to_waitlist з нової функції eat_at_restaurant, визначеної в корені крейта. Ці шляхи є правильними, але залишилася інша проблема, яка перешкоджає компілюванню цього прикладу "як є". Ми пояснимо, чому, трохи пізніше.

Функція eat_at_restaurant є частиною публічного API нашого бібліотечного крейта, тому ми позначимо її ключевим словом pub. Детальніше про pub йтиметься у підрозділі "Надання доступу до шляхів за допомогою ключового слова <1>pub</1> .

Файл: src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Блок коду 7-3: виклик функції add_to_waitlist за допомогою абсолютного та відносного шляхів

Коли ми вперше ми викликаємо функцію add_to_waitlist в eat_at_restaurant, то використовуємо абсолютний шлях. Функція add_to_waitlist визначена у тому ж крейті, що й eat_at_restaurant, тобто ми можемо використати ключове слово crate на початку абсолютного шляху. Потім ми додаємо кожен з вкладених модулів, доки не не вкажемо весь шлях до add_to_waitlist. Уявіть собі файлову систему з такою ж структурою: ми повинні вказати шлях /front_of_house/hosting/add_to_waitlist, щоб запустити програму add_to_waitlist; використання назви crate, щоб почати з кореня, схожий на використання /, щоб почати шлях з кореня файлової системи у вашій оболонці.

Коли ми вдруге викликаємо add_to_waitlist у eat_at_restaurant, то використовуємо відносний шлях. Шлях починається з front_of_house, назви модуля, визначеного на тому ж рівні дерева модулів, що й eat_at_restaurant. Тут аналогом з файлової системи буде використання шляху front_of_house/hosting/add_to_waitlist. Початок з назви модуля означає, що шлях є відносним.

Рішення, використовувати відносний або абсолютний шлях, вам доведеться робити, виходячи з від вашого проєкту, і залежить від того, чи код, що визначає елемент, окремо від коду, що використовує його, чи разом. Наприклад, якщо ми перемістимо модуль front_of_house і функцію eat_at_restaurant у модуль customer_experience, нам знадобиться оновити абсолютний шлях до add_to_waitlist, але відносний шлях усе ще буде коректним. Однак, якби ми перенесли функцію eat_at_restaurant окремо до модуля з назвою dining, абсолютний шлях до виклику add_to_waitlist залишаться таким самим, але відносний шлях треба буде оновити. Загалом, ми вважаємо за краще вказувати абсолютні шляхи, тому що з більшою ймовірністю ми захочемо перемістити код визначення та виклики елементів незалежно один від одного.

Спробуймо скомпілювати Блок коду 7-3 і дізнатися, чому він досі не компілюється! Помилка, що ми отримуємо, показана у Блоці коду 7-4.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^ private module
  |
note: the module `hosting` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod hosting {
  |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

Блок коду 7-4: помилки компілятора при збірці коду в Блоці коду 7-3

Повідомлення про помилки кажуть, що модуль hosting є приватним. Іншими словами, ми маємо коректні шляхи для модуля hosting і функції add_to_waitlist, але Rust не дозволяє нам використовувати їх, бо немає доступу до приватних частин. У Rust усі елементи (функції, методи, структури, енуми, модулі і константи) за замовчуванням є приватними в батьківських модулях. Якщо ви хочете зробити елемент на кшталт функції чи структури приватним, то розміщуєте його у модулі.

Елементи батьківського модуля не можуть використовувати приватні елементи дочірніх модулів, але елементи дочірніх модулів можуть використовувати елементи у модулях-предках. Так зроблено, щоб дочірні модулі огортали і ховали деталі своєї реалізації, але дочірні модулі можуть бачити контекст, у якому їх визначено. Щоб розвинути нашу метафору, уявіть собі правила приватності як бек-офіс ресторану: те, що там відбувається, недоступно для клієнтів ресторану, але менеджери можуть бачити і робити все у ресторані, яким вони керують.

У Rust вирішено зробити модульну систему, де деталі реалізації є прихованими за замовчуванням. Таким чином, ви знаєте, які частини внутрішнього коду ви можете змінити, не зламавши зовнішній код. Однак Rust надає вам можливість виставити внутрішні частини коду дочірніх модулів для зовнішніх модулів-предків за допомогою ключового слова pub, щоб зробити елемент публічним.

Надання доступу до шляхів за допомогою ключового слова pub

Повернімося до помилки у Блоці коду 7-4, яка каже нам, що модуль hosting є приватним. Ми хочемо, щоб функція eat_at_restaurant в батьківському модулі мала доступ до функції add_to_waitlist в дочірньому модулі, тож ми позначили модуль hosting за допомогою ключового слова pub, як показано в Блоці коду 7-5.

Файл: src/lib.rs

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Блок коду 7-5: проголошення модуля hosting як pub, щоб використовувати його з eat_at_restaurant

На жаль, код у Блоці коду 7-5 все ще призводить до помилки, як це показано в Блоці коду 7-6.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^ private function
  |
note: the function `add_to_waitlist` is defined here
 --> src/lib.rs:3:9
  |
3 |         fn add_to_waitlist() {}
  |         ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

Блок коду 7-6: помилки компілятора від збірки коду у Блоці коду 7-5

Що сталося? Додавання ключового слова pub перед mod hosting робить модуль публічним. Після цієї зміни, якщо ми маємо доступ front_of_house, ми можемо отримати доступ до hosting. Але вміст hosting все ще є приватним; зробивши модуль публічним, ми робимо публічним його вміст. Ключове слово pub для модуля дозволяє коду в модулях-предках тільки посилатися на нього, а не мати доступ до його внутрішнього коду. Оскільки модулі є контейнерами, ми багато не зробимо, лише зробивши модуль публічним; ми маємо піти далі і також зробити ще один або більше елементів модуля публічними.

Помилки у Блоці коду 7-6 кажуть, що функція add_to_waitlist є приватною. Правила приватності застосовуються до структур, енумів, функцій і методів, як і до модулів.

Також зробімо публічною функцію add_to_waitlist, додавши ключове слово pub перед її визначенням, як у Блоці коду 7-7.

Файл: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Блок коду 7-7: Додавання ключового слова pub до mod hosting і fn add_to_waitlist дозволяє нам викликати функцію з eat_at_restaurant

Тепер код скомпілюється! Щоб побачити, чому додавання ключового слова pub дозволяє нам використовувати ці шляхи у add_to_waitlist відповідно до правил приватності, розгляньмо абсолютні та відносні шляхи.

Абсолютний шлях ми починаємо з crate, кореня дерева модулів нашого крейта. Модуль front_of_house визначено в корені крейта. Оскільки функція eat_at_restaurant визначена в тому ж модулі, що й front_of_house (тобто, eat_at_restaurant та front_of_house є сестрами), то поки front_of_house не є публічним, ми можемо посилатися на front_of_house лише з eat_at_restaurant. Наступний модуль hosting позначений як pub. Ми маємо доступ до батьківського модуля hosting, тож маємо доступ до hosting. Нарешті, функція add_to_waitlist позначена як pub і ми маємо доступ до її батьківського модуля, тож виклик функції працює!

У відносному шляху логіка така ж сама як і в абсолютному, окрім першого кроку: замість починати з кореня крейта, шлях починається з front_of_house. Модуль front_of_house визначено в тому ж модулі, що й eat_at_restaurant, тому відносний шлях, що починається з модуля, в якому визначено eat_at_restaurant, працює. Потім, оскільки hosting і add_to_waitlist позначені як pub, решта шляху працює, і цей виклик функції - коректний!

Якщо ви плануєте поділитися своєю бібліотекою, щоб інші проєкти могли використовувати ваш код, ваш публічний API - це ваш контракт з користувачами вашого крейта, який визначає, як вони можуть взаємодіяти з вашим кодом. Існує багато міркувань щодо управління змінами у вашому публічному API для полегшення залежності від Вашого крейта. Ці міркування не лежать за межами цієї книжки; якщо вам цікава ця тема, дивіться Керівництво з API Rust.

Кращі практики для пакунків з двійковим крейтом і бібліотекою

Ми згадували, що пакунок може містити одночасно корінь як двійкового крейта src/main.rs, так і корінь бібліотечного крейта src/lib.rs, і обидва крейти матимуть за замовчуванням назву пакету. Зазвичай, пакунки, створені за таким шаблоном, з бібліотекою і двійковим крейтом, матимуть у двійковому крейті лише код, потрібний для запуску виконуваного коду з бібліотечного крейта. Це дозволяє іншим проєктам отримувати максимум функціоналу, який надає пакунок, бо бібліотечний крейт можна використовувати спільно.

Дерево модулів має бути визначеним в src/lib.rs. Тоді будь-які публічні елементи можна використовувати у двійковому крейті, починаючи шлях з назви пакунку. Двійковий крейт стає таким самим користувачем бібліотечного крейта, як і абсолютно зовнішній крейт, що використовує бібліотечний крейт: він може користуватися лише публічним API. Це допомагає вам розробити хороший API; ви не лише його автор, але також і користувач!

У Розділі 12ми покажемо цю практику організації крейта у програмі командного рядка, що міститиме як двійковий крейт, так і бібліотечний крейт.

Початок відносних шляхів з super

Ми можемо створювати відносні шляхи, які починаються в батьківському модулі, а не в поточному чи корені крейта, застосувавши super на початку шляху. Це схоже на .. на початку шляху в файловій системі. За допомогою super ми можемо посилатися на елемент, що, як ми знаємо, знаходиться в батьківському модулі, що спрощує зміну дерева модулів, коли модуль є тісно пов'язаним із батьком, але батьківський елемент може бути переміщений в інше місця дерева модулів.

Розглянемо код у Блоці коду 7-8, який моделює ситуацію, в якій шеф-кухар виправляє неправильне замовлення і особисто приносить його клієнту. Функція fix_incorrect_order, визначена у модулі back_of_house викликає функцію deliver_order, визначену в батьківському модулі, вказавши шлях до deliver_order, починаючи з super:

Файл: src/lib.rs

fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}

Блок коду 7-8: виклик функції за допомогою відносного шляху, що починається з super

Функція fix_incorrect_order знаходиться в модулі back_of_house, тож ми можемо використатися super, щоб перейти до батьківсього модуля back_of_house, який у цьому випадку є коренем, crate. Звідси ми шукаємо deliver_order і знаходимо її. Успіх! Ми гадаємо, що модуль back_of_house і функція deliver_order найімовірніше залишатимуться у такому відношенні одне до одного і будуть переміщені разом, якщо ми вирішимо реорганізувати дерево модулів крейта. Таким чином, ми скористалися super, щоб мати менше місць, де треба буде для оновлювати код у майбутньому, якщо цей код перемістять в інший модуль.

Робимо структури і енуми публічними

Також ми можемо використовувати pub для визначення структур та енумів публічними, але є додаткові особливості використання pub зі структурами та енумами. Якщо ми використовуємо pub перед визначенням структури, ми робимо структуру публічною, але поля структури все одно будуть приватними. Ми можемо зробити публічним чи ні кожне поле окремо в кожному конкретному випадку. У Блоці коду 7-9 ми визначили публічну структуру back_of_house::Breakfast з публічним полем toast, але приватним полем seasonal_fruit. Це моделює ситуацію в ресторані, коли покупець може обрати тип хліба, що додається до їжі, але кухар вирішує, які фрукти йдуть до їжі залежно від сезону і наявності. Доступні фрукти швидко змінюються, тому клієнти не можуть вибрати фрукти і навіть побачити, які фрукти вони отримають.

Файл: src/lib.rs

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // meal.seasonal_fruit = String::from("blueberries");
}

Блок коду 7-9: структура, деякі поля якої є публічними, а деякі приватними

Оскільки поле toast у структурі back_of_house::Breakfast є публічним, у eat_at_restaurant ми можемо писати та читати поле toast, використовуючи точку. Зверніть увагу, що ми не можемо використовувати поле seasonal_fruit у eat_at_restaurant, тому що seasonal_fruit є приватним. Спробуйте розкоментувати рядок, що змінює значення поля seasonal_fruit, щоб подивитися, яку помилку ви отримуєте!

Крім того, зауважте, що оскільки back_of_house::Breakfast має приватне поле, структура має надавати публічну асоційовану функцію, що створює екземпляр Breakfast (тут ми назвали її summer). Якби Breakfast не мав такої функції, ми не могли б створити екземпляр Breakfast у eat_at_restaurant, бо не могли б виставити значення приватного поля seasonal_fruit у eat_at_restaurant.

На відміну від цього, якщо ми робимо енум публічним, усі його варіанти є публічними. Потрібно лише одне ключове слово pub перед enum, як показано в Блоці коду 7-10.

Файл: src/lib.rs

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Блок коду 7-10: позначення енума публічним робить публічними усі його варіанти

Оскільки ми зробили енум Appetizer публічним, то можемо використовувати варіанти Soup та Salad у eat_at_restaurant.

Енуми не дуже корисні, коли їхні варіанти не є публічними; було б набридливим анотувати всі варіанти енуму як pub у будь-якому випадку, то за замовчуванням варіанти переліку є публічними. Структури часто є корисними без публічних полів, тож поля структур слідують загальному правилу, що все є приватним за замовчуванням, якщо не анотовано як pub.

Є ще одна ситуація, пов’язана з pub, про яку ми не розповіли, і це остання деталь системи модулів: ключове слово use. Ми спершу розповімо про use, а потім покажемо, як комбінувати pub і use.