сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Основы Rust

Модули и видимость в Rust

Модули, видимость и фасад

Вопросы про модули выглядят скучно, но отвечают на них плохо. Спрашивают, что видно по умолчанию, чем pub(crate) отличается от pub, зачем pub use. За этим стоит понимание границ: что вы обещаете пользователям библиотеки, а что оставляете себе.

Стержень: всё приватно, пока не написан pub; модуль - граница инкапсуляции, а публичный API можно собрать фасадом через переэкспорт.

// Формулировки: «какая видимость по умолчанию?», «что даёт pub(crate)?», «зачем pub use в lib.rs?»

Приватно по умолчанию

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

Видимость структуры и её полей задаётся отдельно. pub struct Config с приватными полями снаружи не собрать литералом: остаются конструктор вроде new и методы. Это и есть инкапсуляция - вы сохраняете инварианты и свободу менять раскладку полей, не ломая пользователей.

pub struct Config {
    url: String, // приватное поле
}
impl Config {
    pub fn new(url: String) -> Self { Self { url } }
}
pub
открывает элемент наружу текущего модуля
pub(crate)
видно всему крейту, но не внешним пользователям

Пути и дерево модулей

Пути бывают абсолютные - от crate:: или от имени внешнего крейта, и относительные: self:: для текущего модуля, super:: для родительского. В тестовом подмодуле почти всегда пишут use super::*, чтобы затащить проверяемый код.

Важная деталь, которую часто путают: дерево модулей задаёт mod, а файлы - лишь удобная раскладка. Модуль не обязан быть отдельным файлом, и наоборот - файл сам по себе в дерево не попадёт, пока его не объявили через mod. use ничего не подключает, он только сокращает путь к уже существующему элементу.

mod
объявление модуля: включает его в дерево крейта
use
сокращение пути; в дерево ничего не добавляет

Фасад через pub use

Внутри библиотеки удобно резать код на модули, но пользователю не хочется писать crate::internal::config::v2::Config. Решение - переэкспорт: pub use crate::internal::config::Config; в lib.rs. Тип остаётся тем же самым, просто появляется короткий публичный путь.

Выигрыш двойной. Пользователи видят плоский понятный API, а вы сохраняете право перекладывать внутренние модули: пока фасад на месте, чужой код не ломается и мажорную версию выпускать не нужно.

// Соседний инструмент - #[cfg(test)] над mod tests: такой модуль компилируется только при сборке тестов и не раздувает релизный бинарник.

// lib.rs
mod internal;
pub use internal::config::Config;
переэкспорт
pub use: короткий публичный путь к внутреннему элементу
фасад
плоский публичный API поверх внутренней структуры модулей

Как отвечать: «Какая видимость у элементов по умолчанию и зачем pub(crate)?»

По умолчанию всё приватно: элемент виден своему модулю и его потомкам. pub открывает его наружу, а pub(crate) - промежуточный вариант: видно любому модулю внутри крейта, но не пользователям библиотеки. Я беру его для общих внутренних хелперов - они доступны там, где нужны, но не становятся публичным обещанием, которое потом придётся тащить по семверу. А публичный API собираю фасадом через pub use, чтобы внутреннюю раскладку модулей можно было менять свободно.

Ответ связывает видимость с семвером и поддержкой библиотеки это уровень человека, который выпускал релизы, а не просто читал главу про модули.

На чём валятся

  • Думают, что pub у структуры автоматически открывает поля. Их видимость задаётся отдельно.
  • Путают mod и use: первый включает модуль в дерево, второй лишь сокращает путь.
  • Считают, что модуль обязан быть файлом - дерево задаёт mod, а не файловая система.
  • Не знают pub(crate) и делают публичным всё, что нужно соседнему модулю.
  • Не понимают, зачем #[cfg(test)], и объясняют доступ тестов к приватному именно этим атрибутом - на самом деле дело во вложенности.

Проверьте себя

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

  1. #rs_modules1 / 5
    Что даёт pub(crate) на функции библиотеки?
    A)Функция видна только текущему модулю и его потомкам
    B)Функция экспортируется, но помечается устаревшей для внешних пользователей
    C)Функция видна любому модулю крейта, но не попадает во внешний API
    D)Функция видна другим крейтам того же воркспейса
    показать ответ и разбор
    +C)Функция видна любому модулю крейта, но не попадает во внешний API

    // разбор: pub(crate) снимает границу между модулями внутри крейта и одновременно закрывает элемент снаружи: пользователь библиотеки его не увидит. Это рабочая середина между приватным и pub — общие хелперы доступны своим модулям, а публичное обещание, которое придётся тащить по семверу, не появляется.

  2. #rs_modules2 / 5
    Структура объявлена pub, а её поля — нет. Что это даёт снаружи крейта?
    A)Ничего: pub на типе автоматически открывает и поля
    B)Поля доступны на чтение, но не на запись
    C)Тип бесполезен снаружи: без полей его не создать
    D)Тип виден, поля недоступны — только через методы
    показать ответ и разбор
    +D)Тип виден, поля недоступны — только через методы

    // разбор: Видимость полей задаётся отдельно, поэтому внешний код не соберёт структуру литералом и не полезет внутрь: остаются конструктор вроде new и геттеры. Так автор сохраняет инварианты и свободу менять раскладку полей, не ломая пользователей — приватное поле не часть публичного контракта.

  3. #rs_modules3 / 5
    Зачем тестам в том же файле атрибут #[cfg(test)] над mod tests?
    A)Модуль компилируется только при сборке тестов
    B)Без него тесты не видят приватные функции модуля
    C)Атрибут запускает тесты параллельно в отдельных потоках
    D)Он подключает dev-зависимости в обычную сборку
    показать ответ и разбор
    +A)Модуль компилируется только при сборке тестов

    // разбор: cfg(test) — условная компиляция: при обычной сборке модуль и его зависимости просто не существуют, поэтому тестовый код не раздувает бинарник и не тянет лишнее. Доступ к приватным элементам даёт не он, а расположение: вложенный модуль видит внутренности родителя, отсюда use super::*.

  4. #rs_modules4 / 5
    Библиотека делает pub use crate::inner::Config; в lib.rs. Что это меняет?
    A)Создаётся копия типа в корне: два разных типа с одним именем
    B)Тип доступен по короткому пути крейта, внутренняя раскладка модулей скрыта
    C)Модуль inner становится публичным целиком
    D)Ничего для пользователей: pub use влияет только на текущий файл
    показать ответ и разбор
    +B)Тип доступен по короткому пути крейта, внутренняя раскладка модулей скрыта

    // разбор: pub use — переэкспорт: элемент становится частью публичного API по новому пути, оставаясь одним и тем же типом. Так строят фасад: пользователи пишут crate::Config, а внутренние модули можно перекладывать, не ломая их код. Обычный use виден только в своём модуле и наружу ничего не добавляет.

  5. #rs_modules5 / 5
    Чем крейт отличается от модуля?
    A)Крейт — единица компиляции и распространения, модуль — узел дерева внутри него
    B)Крейт — это файл, а модуль — папка с файлами
    C)Крейт — это библиотека, модуль — исполняемая часть проекта
    D)Крейт в проекте один, модулей сколько угодно
    показать ответ и разбор
    +A)Крейт — единица компиляции и распространения, модуль — узел дерева внутри него

    // разбор: Крейт компилируется целиком и целиком публикуется: бинарный (с main) или библиотечный (с lib.rs). Внутри него mod строит дерево имён и границы видимости, и файлы тут вторичны — модуль может лежать в отдельном файле, в папке или прямо в фигурных скобках. В рабочем пространстве крейтов много, каждый собирается отдельно и версионируется сам по себе.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.