Шляхи для посилання на елемент у дереві модулів (Paths for Referring to an Item in the Module Tree)
Щоб показати Rust, де знайти елемент у дереві модулів, ми використовуємо шлях так само, як використовуємо шлях під час навігації файловою системою. Щоб викликати функцію, нам потрібно знати її шлях.
Шлях може мати дві форми:
- Абсолютний шлях — це повний шлях, що починається від кореня крейту; для коду із зовнішнього крейту абсолютний шлях починається з назви крейту, а для коду з поточного крейту він починається з літерала
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” ми детальніше розглянемо pub.
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();
}
Перший раз, коли ми викликаємо функцію 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();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| 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();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| 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` (lib) due to 2 previous errors
Повідомлення про помилки кажуть, що модуль hosting є приватним. Іншими словами, ми маємо правильні шляхи для модуля hosting і функції add_to_waitlist, але Rust не дозволить нам використовувати їх, тому що він не має доступу до приватних секцій. У Rust усі елементи (функції, методи, структури, перелічення, модулі та константи) за замовчуванням є приватними для батьківських модулів. Якщо ви хочете зробити елемент, наприклад функцію або структуру, приватним, ви поміщаєте його в модуль.
Елементи в батьківському модулі не можуть використовувати приватні елементи всередині дочірніх модулів, але елементи в дочірніх модулях можуть використовувати елементи у своїх модулів-предків. Це тому, що дочірні модулі обгортають і приховують деталі своєї реалізації, але дочірні модулі можуть бачити контекст, у якому вони визначені. Продовжуючи нашу метафору, подумайте про правила приватності як про службове приміщення ресторану: те, що там відбувається, є приватним для відвідувачів ресторану, але офісні менеджери можуть бачити і робити все в ресторані, яким вони керують.
Rust вирішив, щоб система модулів працювала саме так, аби приховування внутрішніх деталей реалізації було типовою поведінкою. Таким чином ви знаєте, які частини внутрішнього коду можете змінювати, не ламаючи зовнішній код. Однак Rust таки надає вам можливість відкривати внутрішні частини коду дочірніх модулів для зовнішніх модулів-предків, використовуючи ключове слово pub, щоб зробити елемент публічним.
Відкриття шляхів за допомогою ключового слова pub
Повернімося до помилки в Лістингу 7-4, яка сказала нам, що модуль hosting є приватним. Ми хочемо, щоб функція eat_at_restaurant у батьківському модулі мала доступ до функції add_to_waitlist у дочірньому модулі, тому ми позначаємо модуль hosting ключовим словом pub, як показано в Лістингу 7-5.
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
// -- snip --
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 усе ще призводить до помилок компілятора, як показано в Лістингу 7-6.
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:10:37
|
10 | 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:13:30
|
13 | 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` (lib) due to 2 previous errors
Що сталося? Додавання ключового слова pub перед mod hosting робить модуль публічним. З цією зміною, якщо ми можемо отримати доступ до front_of_house, ми можемо отримати доступ до hosting. Але вміст hosting усе ще приватний; зробити модуль публічним не означає зробити публічним його вміст. Ключове слово pub для модуля лише дозволяє коду в його модулях-предках посилатися на нього, але не отримувати доступ до його внутрішнього коду. Оскільки модулі є контейнерами, мало що можна зробити, лише зробивши модуль публічним; нам потрібно піти далі й обрати також зробити один або кілька елементів усередині модуля публічними.
Помилки в Лістингу 7-6 кажуть, що функція add_to_waitlist є приватною. Правила приватності застосовуються також до структур, перелічень, функцій і методів, а також до модулів.
Давайте також зробимо функцію add_to_waitlist публічною, додавши ключове слово pub перед її визначенням, як у Лістингу 7-7.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
Тепер код скомпілюється! Щоб побачити, чому додавання ключового слова pub дозволяє нам використовувати ці шляхи в eat_at_restaurant з погляду правил приватності, давайте подивимося на абсолютний і відносний шляхи.
В абсолютному шляху ми починаємо з crate, кореня дерева модулів нашого крейту. Модуль front_of_house визначений у корені крейту. Хоча front_of_house не є публічним, оскільки функція eat_at_restaurant визначена в тому самому модулі, що й front_of_house (тобто eat_at_restaurant і 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, щоб людям було легше залежати від вашого крейту. Ці міркування виходять за межі цього підручника; якщо вас цікавить ця тема, дивіться Rust API Guidelines.
Найкращі практики для пакетів із бінарним і бібліотечним крейтом
Ми згадували, що пакет може містити як корінь бінарного крейту 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.
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
Функція 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. Це моделює випадок у ресторані, коли клієнт може вибрати тип хліба, що подається до страви, але кухар вирішує, який фрукт супроводжує страву, залежно від того, що є в сезоні та в наявності. Доступний фрукт швидко змінюється, тому клієнти не можуть вибрати фрукт або навіть побачити, який фрукт вони отримають.
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");
}
Оскільки поле 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.
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;
}
Оскільки ми зробили перелічення Appetizer публічним, ми можемо використовувати варіанти Soup і Salad у eat_at_restaurant.
Перелічення не дуже корисні, якщо їхні варіанти не є публічними; було б незручно щоразу позначати всі варіанти перелічення pub, тому типовим значенням для варіантів перелічення є публічність. Структури часто корисні без публічності їхніх полів, тому поля структур підпорядковуються загальному правилу: усе за замовчуванням приватне, якщо не позначено pub.
Є ще одна ситуація, пов’язана з pub, яку ми не розглядали, і це наша остання можливість системи модулів: ключове слово use. Спочатку ми окремо розглянемо use, а потім покажемо, як поєднати pub і use.