Введение
Добро пожаловать в Пути для обращения к элементу в дереве модулей. Эта лабораторная работа является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.
В этой лабораторной работе мы узнаем, что пути в Rust используются для обращения к элементам в дереве модулей и могут иметь форму абсолютных путей или относительных путей.
Путь для обращения к элементу в дереве модулей
Чтобы показать Rust, где искать элемент в дереве модулей, мы используем путь так же, как мы используем путь при навигации по файловой системе. Чтобы вызвать функцию, нам нужно знать ее путь.
Путь может быть в двух формах:
- абсолютный путь — это полный путь, начинающийся с корня коробки; для кода из внешней коробки абсолютный путь начинается с имени коробки, а для кода из текущей коробки он начинается с литерала
crate. - относительный путь начинается с текущего модуля и использует
self,superили идентификатор в текущем модуле.
И абсолютный, и относительный пути заканчиваются одним или более идентификаторами, разделенными двойными двоеточиями (::).
Вернемся к Listing 7-1. Предположим, что мы хотим вызвать функцию add_to_waitlist. Это то же самое, что и вопрос: какой путь к функции add_to_waitlist? Listing 7-3 содержит Listing 7-1 с удаленными некоторыми модулями и функциями.
Мы покажем два способа вызвать функцию add_to_waitlist из новой функции eat_at_restaurant, определенной в корне коробки. Эти пути правильные, но остается еще одна проблема, которая препятствует компиляции этого примера в текущем виде. Мы объясним, почему это так чуть позже.
Функция eat_at_restaurant является частью публичного API нашей библиотеки-коробки, поэтому мы помечаем ее ключевым словом pub. В разделе "Раскрытие путей с использованием ключевого слова pub" мы рассмотрим pub более подробно.
Имя файла: src/lib.rs
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Абсолютный путь
crate::front_of_house::hosting::add_to_waitlist();
// Относительный путь
front_of_house::hosting::add_to_waitlist();
}
Listing 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; использование имени коробки для начала пути от корня коробки аналогично использованию / для начала пути от корня файловой системы в вашей оболочке.
Во второй раз, когда мы вызываем 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 останется тем же, но относительный путь нужно будет обновить. В целом, наша предпочтительность — указывать абсолютные пути, потому что более вероятно, что мы захотим перемещать определения кода и вызовы элементов независимо друг от друга.
Попробуем скомпилировать Listing 7-3 и выясним, почему он еще не скомпилируется! Ошибки, которые мы получаем, показаны в Listing 7-4.
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: модуль `hosting` является приватным
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ приватный модуль
|
note: модуль `hosting` определен здесь
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: модуль `hosting` является приватным
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ приватный модуль
|
note: модуль `hosting` определен здесь
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
Listing 7-4: Ошибки компилятора при сборке кода из Listing 7-3
Сообщения об ошибках говорят, что модуль hosting является приватным. Другими словами, у нас правильные пути к модулю hosting и функции add_to_waitlist, но Rust не позволяет нам их использовать, потому что у него нет доступа к приватным разделам. В Rust все элементы (функции, методы, структуры, перечисления, модули и константы) по умолчанию приватны для родительских модулей. Если вы хотите сделать элемент, такой как функция или структура, приватным, вы помещаете его в модуль.
Элементы в родительском модуле не могут использовать приватные элементы внутри дочерних модулей, но элементы в дочерних модулях могут использовать элементы в их предках. Это потому, что дочерние модули заключают и скрывают свои детали реализации, но дочерние модули могут видеть контекст, в котором они определены. Продолжая нашу метафору, представьте, что правила приватности похожи на внутреннюю часть ресторана: то, что там происходит, приватно для посетителей ресторана, но менеджеры офиса могут видеть и делать все в ресторане, который они управляют.
Rust выбрал, чтобы модульная система работала именно так, чтобы скрытие внутренних деталей реализации было по умолчанию. Таким образом, вы знаете, какие части внутреннего кода можно изменить, не нарушая внешнего кода. Однако Rust все-таки дает вам возможность раскрыть внутренние части кода дочерних модулей для внешних предков, используя ключевое слово pub, чтобы сделать элемент публичным.
Раскрытие путей с использованием ключевого слова pub
Вернемся к ошибке в Listing 7-4, которая сообщила нам, что модуль hosting является приватным. Мы хотим, чтобы функция eat_at_restaurant в родительском модуле имела доступ к функции add_to_waitlist в дочернем модуле, поэтому мы помечаем модуль hosting ключевым словом pub, как показано в Listing 7-5.
Имя файла: src/lib.rs
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
--snip--
Listing 7-5: Объявление модуля hosting как pub, чтобы использовать его из eat_at_restaurant
К сожалению, код в Listing 7-5 по-прежнему вызывает ошибки компиляции, как показано в Listing 7-6.
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: функция `add_to_waitlist` является приватной
--> src/lib.rs:9:37
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ приватная функция
|
note: функция `add_to_waitlist` определена здесь
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: функция `add_to_waitlist` является приватной
--> src/lib.rs:12:30
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ приватная функция
|
note: функция `add_to_waitlist` определена здесь
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
Listing 7-6: Ошибки компиляции при сборке кода из Listing 7-5
Что произошло? Добавление ключевого слова pub перед mod hosting делает модуль публичным. С этой изменением, если мы можем получить доступ к front_of_house, мы можем получить доступ к hosting. Но содержимое hosting по-прежнему приватно; сделать модуль публичным не делает его содержимое публичным. Ключевое слово pub на модуле позволяет только коду в его предках обращаться к нему, но не получать доступ к его внутреннему коду. Поскольку модули являются контейнерами, мы не можем сделать много, просто сделав модуль публичным; нам нужно пойти дальше и выбрать, чтобы один или несколько элементов внутри модуля были также публичными.
Ошибки в Listing 7-6 говорят, что функция add_to_waitlist является приватной. Правила приватности применяются к структурам, перечислениям, функциям и методам, а также модулям.
Давайте также сделаем функцию add_to_waitlist публичной, добавив ключевое слово pub перед ее определением, как в Listing 7-7.
Имя файла: src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
--snip--
Listing 7-7: Добавление ключевого слова pub к mod hosting и fn add_to_waitlist позволяет нам вызывать функцию из eat_at_restaurant.
Теперь код скомпилируется! Чтобы понять, почему добавление ключевого слова pub позволяет нам использовать эти пути в add_to_waitlist в отношении правил приватности, рассмотрим абсолютный и относительные пути.
В абсолютном пути мы начинаем с 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 по адресу https://rust-lang.github.io/api-guidelines.
Лучшие практики для пакетов с бинарником и библиотекой
Мы упоминали, что пакет может содержать как корень бинарной коробки
src/main.rs, так и корень библиотечной коробкиsrc/lib.rs, и обе коробки по умолчанию будут иметь имя пакета. Обычно пакеты с такой структурой, содержащие как библиотеку, так и бинарную коробку, будут иметь достаточно кода в бинарной коробке, чтобы запустить исполняемый файл, который вызывает код с использованием библиотечной коробки. Это позволяет другим проектам использовать наиболее полезные функции, которые предоставляет пакет, потому что код библиотечной коробки можно разделить.Дерево модулей должно быть определено в
src/lib.rs. Затем любые публичные элементы могут быть использованы в бинарной коробке, начиная пути с имени пакета. Бинарная коробка становится пользователем библиотечной коробки так же, как и совершенно внешняя коробка использовала бы библиотечную коробку: она может использовать только публичный API. Это помогает вам спроектировать хороший API; не только вы являетесь автором, но и клиентом!В главе 12 мы продемонстрируем эту организационную практику с помощью командной строки программы, которая будет содержать как бинарную коробку, так и библиотечную коробку.
Начало относительных путей с использованием super
Мы можем создавать относительные пути, которые начинаются в родительском модуле, а не в текущем модуле или корне коробки, используя super в начале пути. Это похоже на то, как начинается путь в файловой системе с синтаксиса ... Использование super позволяет нам ссылаться на элемент, который мы знаем, находится в родительском модуле, что может облегчить переупорядочивание дерева модулей, когда модуль тесно связан с родителем, но в будущем родитель может быть перемещен в другое место в дереве модулей.
Рассмотрим код в Listing 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() {}
}
Listing 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 перед определением структуры, мы делаем структуру публичной, но поля структуры по-прежнему будут приватными. Мы можем сделать каждое поле публичным или нет в зависимости от обстоятельств. В Listing 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() {
// Закажите завтрак летом с ржаным хлебом
let mut meal = back_of_house::Breakfast::summer("Rye");
// Изменим решение о типе хлеба
meal.toast = String::from("Wheat");
println!("Я бы хотел {} хлеба, пожалуйста", meal.toast);
// Следующая строка не скомпилируется, если мы раскомментируем ее; мы не
// имеем права видеть или изменять сезонный фрукт, который приходит
// с едой
// meal.seasonal_fruit = String::from("blueberries");
}
Listing 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, как показано в Listing 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;
}
Listing 7-10: Назначение перечисления как публичного делает все его варианты публичными.
Поскольку мы сделали перечисление Appetizer публичным, мы можем использовать варианты Soup и Salad в eat_at_restaurant.
Перечисления не очень полезны, если их варианты не являются публичными; было бы раздражающим каждый раз аннотировать все варианты перечисления с pub, поэтому по умолчанию варианты перечислений являются публичными. Структуры часто полезны без публичности их полей, поэтому поля структур следуют общему правилу: все по умолчанию приватно, если не аннотировано с pub.
Есть еще одна ситуация, связанная с pub, которую мы не рассмотрели, и это наша последняя особенность модульной системы: ключевое слово use. Мы сначала рассмотрим use сам по себе, а затем покажем, как комбинировать pub и use.
Резюме
Поздравляем! Вы завершили лабораторную работу "Пути для обращения к элементу в дереве модулей". Вы можете выполнить больше лабораторных работ в LabEx, чтобы улучшить свои навыки.