Пути для обращения к элементу в дереве модулей
Чтобы показать 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. Можно представить файловую систему с
такой же структурой: чтобы запустить программу add_to_waitlist, мы указали
бы путь /front_of_house/hosting/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 все элементы (функции, методы, структуры, enum, модули и константы) по
умолчанию приватны для родительских модулей. Если вы хотите сделать элемент,
например функцию или структуру, приватным, вы помещаете его в модуль.
Элементы в родительском модуле не могут использовать приватные элементы внутри дочерних модулей, но элементы в дочерних модулях могут использовать элементы в своих модулях-предках. Это связано с тем, что дочерние модули оборачивают и скрывают детали своей реализации, но могут видеть контекст, в котором они определены. Продолжая нашу метафору, думайте о правилах приватности как о служебном кабинете ресторана: то, что там происходит, скрыто от клиентов ресторана, но менеджеры служебной части могут видеть и делать все в ресторане, которым они управляют.
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 приватная.
Правила приватности применяются к структурам, enum, функциям и методам так же,
как и к модулям.
Сделаем функцию 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 являются
соседними элементами), поэтому мы можем ссылаться на 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.
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, чтобы в
будущем было меньше мест, где нужно обновлять код, если этот код переместится
в другой модуль.
Как сделать структуры и enum публичными
Мы также можем использовать pub, чтобы обозначить структуры и enum как
публичные, но в использовании pub со структурами и enum есть несколько
дополнительных деталей. Если мы используем 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.
В отличие от этого, если мы делаем enum публичным, все его варианты тоже
становятся публичными. Нам нужно поставить 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;
}
Поскольку мы сделали enum Appetizer публичным, мы можем использовать варианты
Soup и Salad в eat_at_restaurant.
Enum не очень полезны, если их варианты не публичны; было бы утомительно
помечать все варианты enum с помощью pub в каждом случае, поэтому по
умолчанию варианты enum являются публичными. Структуры часто полезны и без
публичных полей, поэтому поля структур следуют общему правилу: все приватно по
умолчанию, если не помечено pub.
Есть еще одна ситуация, связанная с pub, которую мы не рассмотрели, и это
последняя возможность системы модулей: ключевое слово use. Сначала мы
рассмотрим use отдельно, а затем покажем, как сочетать pub и use.