Пути для обращения к элементу в дереве модулей
Чтобы указать 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 с использованием абсолютного и относительного путейВ первый раз мы вызываем функцию 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();
}
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: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 к mod hosting и fn add_to_waitlist позволяет нам вызвать функцию из eat_at_restaurantТеперь код скомпилируется! Чтобы понять, почему добавление ключевого слова pub позволяет нам использовать эти пути в eat_at_restaurant с точки зрения правил конфиденциальности, давайте посмотрим на абсолютный и относительный пути.
В абсолютном пути мы начинаем с crate — корня дерева модулей нашего крейта. Модуль front_of_house определён в корне крейта. Хотя front_of_house не является публичным, потому что функция eat_at_restaurant определена в том же модуле, что и front_of_house (то есть eat_at_restaurant и front_of_house являются sibling-ами), мы можем ссылаться на 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.
Рекомендации для пакетов с бинарным крейтом и библиотекой
Мы упомянули, что пакет может содержать как корень бинарного крейта 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() {}
}
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. Это моделирует ситуацию в ресторане, где клиент может выбрать тип хлеба, который идёт с едой, но повар решает, какой фрукт сопровождает еду, исходя из сезона и наличия. Доступные фрукты быстро меняются, поэтому клиенты не могут выбрать фрукт или даже увидеть, какой фрукт они получат.
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.