Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Пути к элементам в дереве модулей

Чтобы показать 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».

Файл: 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();
}
Листинг 7-3: Вызов функции 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 вызывается по относительному пути. Он начинается с front_of_house — имени модуля, определённого на том же уровне дерева модулей, что и eat_at_restaurant. В файловой системе это был бы путь front_of_house/hosting/add_to_waitlist. Если путь начинается с имени модуля, он относительный.

Какой путь выбрать — относительный или абсолютный, — зависит от проекта, а точнее от того, как вы скорее будете переносить код: определение элемента отдельно от кода, который этот элемент использует, или вместе с ним. Например, если перенести модуль front_of_house и функцию eat_at_restaurant в модуль customer_experience, придётся обновить абсолютный путь к add_to_waitlist, а относительный останется верным. А если перенести в модуль dining одну лишь функцию eat_at_restaurant, то абсолютный путь в вызове 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
Листинг 7-4: Ошибки компиляции кода из листинга 7-3

Сообщения говорят, что модуль hosting приватный. Иными словами, пути к модулю hosting и к функции add_to_waitlist у нас правильные, но Rust не даёт ими воспользоваться: у нас нет доступа к приватным частям. В Rust все элементы (функции, методы, структуры, перечисления, модули и константы) по умолчанию скрыты от родительских модулей. Чтобы сделать элемент вроде функции или структуры приватным, достаточно поместить его в модуль.

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

Rust сделал систему модулей такой, чтобы детали внутренней реализации по умолчанию были скрыты. Так вы знаете, какие части внутреннего кода можно менять, не ломая внешний код. Но Rust позволяет и открыть внутренние части дочерних модулей внешним модулям-предкам — для этого элемент делают публичным ключевым словом pub.

Открываем пути ключевым словом pub

Вернёмся к ошибке из листинга 7-4, которая сообщала, что модуль hosting приватный. Мы хотим, чтобы функция eat_at_restaurant в родительском модуле имела доступ к функции add_to_waitlist в дочернем, поэтому помечаем модуль hosting ключевым словом pub, как в листинге 7-5.

Файл: src/lib.rs
mod front_of_house {
    pub 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();
}
Листинг 7-5: Объявляем модуль 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
Листинг 7-6: Ошибки компиляции кода из листинга 7-5

Что произошло? pub перед mod hosting делает публичным сам модуль. Теперь, если у нас есть доступ к front_of_house, есть доступ и к hosting. Но содержимое hosting по-прежнему приватно: публичный модуль не делает публичным своё содержимое. pub у модуля лишь позволяет коду в модулях-предках ссылаться на сам модуль, но не обращаться к его внутреннему коду. Модули — это контейнеры, и от одного лишь публичного модуля толку мало: нужно пойти дальше и сделать публичными и какие-то элементы внутри него.

Ошибки в листинге 7-6 говорят, что приватна функция add_to_waitlist. Правила приватности распространяются не только на модули, но и на структуры, перечисления, функции и методы.

Сделаем публичной и функцию add_to_waitlist, добавив pub перед её определением, как в листинге 7-7.

Файл: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// -- пропуск --
pub fn eat_at_restaurant() {
    // Абсолютный путь
    crate::front_of_house::hosting::add_to_waitlist();

    // Относительный путь
    front_of_house::hosting::add_to_waitlist();
}
Листинг 7-7: Ключевое слово pub у mod hosting и у fn add_to_waitlist позволяет вызвать функцию из eat_at_restaurant

Теперь код компилируется! Чтобы понять, почему после добавления pub правила приватности позволяют использовать эти пути в eat_at_restaurant, разберём абсолютный и относительный пути.

Абсолютный путь начинается с crate — корня дерева модулей нашего крейта. Модуль front_of_house определён в корне крейта. Он не публичный, но функция eat_at_restaurant определена в том же модуле, что и front_of_house (то есть они соседи), поэтому из eat_at_restaurant на front_of_house ссылаться можно. Дальше идёт модуль 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 (рекомендации по проектированию 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.

Файл: src/lib.rs
fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}
Листинг 7-8: Вызов функции по относительному пути, начинающемуся с super

Функция fix_incorrect_order находится в модуле back_of_house, поэтому с помощью super можно перейти в родительский модуль back_of_house — в данном случае это корень, crate. Там мы ищем deliver_order — и находим. Успех! Мы считаем, что модуль back_of_house и функция deliver_order, скорее всего, так и останутся в тех же отношениях друг с другом и при перестройке дерева модулей крейта будут переноситься вместе. Поэтому мы и использовали super: если этот код когда-нибудь переедет в другой модуль, править придётся меньше мест.

Публичные структуры и перечисления

С помощью pub можно сделать публичными и структуры с перечислениями, но тут есть несколько тонкостей. Если поставить pub перед определением структуры, публичной станет сама структура, но её поля останутся приватными. Каждое поле можно сделать публичным или нет по отдельности. В листинге 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!("I'd like {} toast please", meal.toast);

    // Если раскомментировать следующую строку, код не скомпилируется: нам нельзя
    // ни узнать, ни поменять сезонный фрукт, который подают к завтраку.
    // meal.seasonal_fruit = String::from("blueberries");
}
Листинг 7-9: Структура, у которой часть полей публичные, а часть — приватные

Поле toast в структуре back_of_house::Breakfast публичное, поэтому в eat_at_restaurant его можно читать и записывать через точку. А вот поле seasonal_fruit в eat_at_restaurant использовать нельзя — оно приватное. Попробуйте раскомментировать строку, которая меняет значение seasonal_fruit, и посмотрите, какая будет ошибка!

Заметьте также: раз у back_of_house::Breakfast есть приватное поле, структуре нужна публичная ассоциированная функция, которая создаёт экземпляр Breakfast (здесь она называется summer — «летний»). Без такой функции создать экземпляр Breakfast в eat_at_restaurant было бы невозможно: мы не смогли бы задать значение приватного поля seasonal_fruit.

С перечислениями всё наоборот: если сделать перечисление публичным, публичными становятся все его варианты. Достаточно поставить pub перед ключевым словом enum, как в листинге 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;
}
Листинг 7-10: Если сделать перечисление публичным, публичными станут все его варианты

Перечисление Appetizer (закуска) публичное, поэтому в eat_at_restaurant можно использовать варианты Soup (суп) и Salad (салат).

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

Остался ещё один случай с pub, который мы не разобрали, — он связан с последней частью системы модулей, ключевым словом use. Сначала разберём use само по себе, а потом покажем, как сочетать pub и use.