Использование модулей для управления областью видимости и приватностью

В этом разделе мы поговорим о модулях и других частях системы модулей, а именно: о путях, которые позволяют именовать элементы; о ключевом слове use, которое подключает путь к области видимости; о ключевом слове pub, которое делает элементы общедоступными. Мы также обсудим ключевое слово as, внешние пакеты и оператор *.

Шпаргалка по модулям

Перед тем как вдаваться в детали работы с модулями и путями, мы дадим краткий обзор того, как модули, пути и ключевые слова use и pub работают в компиляторе, и как большинство разработчиков организуют свой код. В этой главе мы рассмотрим всё это на примерах, а пока что у нас есть удобный момент, чтобы дать выжимку о том, как работают крейты.

  • Старт с корня крейта. В начале компиляции крейта, компилятор ищет в корне крейта (обычно это src/lib.rs для библиотечного крейта или src/main.rs для бинарного крейта) код для компиляции.
  • Объявление модулей. В файле корня крейта вы можете объявлять новые модули. Скажем, вы объявляете модуль "garden" с помощью mod garden;. Компилятор будет искать код модуля в следующих местах:
    • В этом же файле между фигурных скобок, которые заменяют точку с запятой после mod garden
    • В файле src/garden.rs
    • В файле src/garden/mod.rs
  • Объявление подмодулей. В любом файле, кроме корня крейта, вы можете объявлять подмодули. К примеру, вы можете объявить mod vegetables; в src/garden.rs. Компилятор будет искать код подмодуля в каталоге с именем родительского модуля в следующих местах:
    • В этом же файле, сразу после mod vegetables и между фигурных скобок, которые заменяют точку с запятой
    • В файле src/garden/vegetables.rs
    • В файле src/garden/vegetables/mod.rs
  • Пути к коду в модулях. После того, как модуль станет частью вашего крейта, и если допускают правила приватности, вы можете ссылаться на код в этом модуле из любого места вашего крейта, используя путь к коду. Например, тип Asparagus в подмодуле vegetables модуля garden будет доступен по пути crate::garden::vegetables::Asparagus.
  • Закрытость и открытость. Код в модуле по умолчанию скрыт от его родительских модулей. Чтобы сделать модуль общедоступным, объявите его как pub mod вместо mod. Чтобы сделать элементы общедоступного модуля тоже общедоступными, используйте pub перед их объявлением.
  • Ключевое слово use. Внутри области видимости использование ключевого слова use создаёт псевдонимы для элементов, чтобы сократить повторение длинных путей. В любой области видимости, из которой можно обратиться к crate::garden::vegetables::Asparagus, вы можете создать псевдоним use crate::garden::vegetables::Asparagus; и после этого вам нужно просто писать Asparagus, чтобы использовать этот тип в этой области видимости.

Мы создали бинарный крейт backyard, который иллюстрирует эти правила. Директория крейта, также названная как backyard, содержит следующие файлы и директории:

backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs

Файл корневого модуля крейта (в нашем случае) — src/main.rs. Вот, что он содержит:

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("Здесь растёт {plant:?}!");
}

Строка pub mod garden; говорит компилятору подключить код, имеющийся в src/garden.rs. Вот подключаемый код:

pub mod vegetables;

А здесь pub mod vegetables; указывает, что код в src/garden/vegetables.rs тоже подключён. Вот подключаемый код:

#[derive(Debug)]
pub struct Asparagus {}

Теперь давайте рассмотрим детали этих правил и покажем их в действии!

Группировка родственного кода в модули

Модули позволяют упорядочивать код внутри крейта для удобочитаемости и лёгкого повторного использования. Модули также позволяют нам управлять приватностью элементов, поскольку код внутри модуля по умолчанию является закрытым. Приватные элементы — это внутренние детали реализации, недоступные для внешнего использования. Мы можем сделать модули и элементы внутри них общедоступными, что позволит внешнему коду использовать их и зависеть от них.

В качестве примера, давайте напишем библиотечный крейт предоставляющий функциональность ресторана. Мы определим сигнатуры функций, но оставим их тела пустыми, чтобы сосредоточиться на организации кода, вместо реализации самого ресторана.

В ресторанной индустрии одни части ресторана называются front of house, а другие — back of house. Front of house — это там, где находятся клиенты; здесь размещаются места клиентов, официанты принимают заказы и оплаты, а бармены делают напитки. Back of house — там, это где повара работают на кухне, работают посудомоечные машины, а менеджеры занимаются хозяйственной деятельностью.

Чтобы структурировать крейт аналогично ресторанной модели, можно организовать размещение его функций во вложенных модулях. Создадим новую библиотеку с именем restaurant выполнив команду cargo new restaurant --lib. Затем вставим код из Листинга 7-1 в src/lib.rs для определения некоторых модулей и сигнатур функций. Это front of house:

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

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

Мы определяем модуль ключевым словом mod, затем пишем название модуля (в данном случае — front_of_house) и размещаем фигурные скобки вокруг тела модуля. Внутри модулей можно иметь другие модули, как мы это сделали с модулями hosting и serving. Модули также могут содержать определения других элементов, таких как структуры, перечисления, константы, трейты или (как в Листинге 7-1) функции.

Используя модули, мы можем группировать родственные определения вместе и пояснять, в чём они являются родственными. Программистам будет легче найти необходимую функциональность в сгруппированном коде, вместо того чтобы искать её в одном общем списке. Программисты, добавляющие новые функции в этот код, будут знать, где разместить код для поддержания порядка в программе.

Earlier, we mentioned that src/main.rs and src/lib.rs are called crate roots. The reason for their name is that the contents of either of these two files form a module named crate at the root of the crate’s module structure, known as the module tree.

В Листинге 7-2 показано дерево модулей для структуры модулей, приведённой в коде Листинга 7-1.

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

Это дерево показывает, как некоторые из модулей вкладываются друг в друга; например, hosting находится внутри front_of_house. Дерево также показывает, что некоторые модули являются "братьями", то есть они определены в одном модуле; hosting и serving — это "братья", которые определены внутри front_of_house. Если модуль A содержится внутри модуля B, мы говорим, что модуль A является потомком модуля B, а модуль B является родителем модуля A. Обратите внимание, что родителем всего дерева модулей является неявный модуль с именем crate.

Дерево модулей может напомнить вам дерево файловой системы на компьютере; это очень удачное сравнение! По аналогии с файловой системой, модули используются для организации кода. И так же, как нам надо искать файлы в директориях, нам требуется способ поиска нужных модулей.