Модули и видимость в 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, остальные разбираются в тренажёре.
- Что даёт pub(crate) на функции библиотеки?A)Функция видна только текущему модулю и его потомкамB)Функция экспортируется, но помечается устаревшей для внешних пользователейC)Функция видна любому модулю крейта, но не попадает во внешний APID)Функция видна другим крейтам того же воркспейса
показать ответ и разбор
+C)Функция видна любому модулю крейта, но не попадает во внешний API// разбор: pub(crate) снимает границу между модулями внутри крейта и одновременно закрывает элемент снаружи: пользователь библиотеки его не увидит. Это рабочая середина между приватным и pub — общие хелперы доступны своим модулям, а публичное обещание, которое придётся тащить по семверу, не появляется.
- Структура объявлена pub, а её поля — нет. Что это даёт снаружи крейта?A)Ничего: pub на типе автоматически открывает и поляB)Поля доступны на чтение, но не на записьC)Тип бесполезен снаружи: без полей его не создатьD)Тип виден, поля недоступны — только через методы
показать ответ и разбор
+D)Тип виден, поля недоступны — только через методы// разбор: Видимость полей задаётся отдельно, поэтому внешний код не соберёт структуру литералом и не полезет внутрь: остаются конструктор вроде new и геттеры. Так автор сохраняет инварианты и свободу менять раскладку полей, не ломая пользователей — приватное поле не часть публичного контракта.
- Зачем тестам в том же файле атрибут #[cfg(test)] над mod tests?A)Модуль компилируется только при сборке тестовB)Без него тесты не видят приватные функции модуляC)Атрибут запускает тесты параллельно в отдельных потокахD)Он подключает dev-зависимости в обычную сборку
показать ответ и разбор
+A)Модуль компилируется только при сборке тестов// разбор: cfg(test) — условная компиляция: при обычной сборке модуль и его зависимости просто не существуют, поэтому тестовый код не раздувает бинарник и не тянет лишнее. Доступ к приватным элементам даёт не он, а расположение: вложенный модуль видит внутренности родителя, отсюда use super::*.
- Библиотека делает pub use crate::inner::Config; в lib.rs. Что это меняет?A)Создаётся копия типа в корне: два разных типа с одним именемB)Тип доступен по короткому пути крейта, внутренняя раскладка модулей скрытаC)Модуль inner становится публичным целикомD)Ничего для пользователей: pub use влияет только на текущий файл
показать ответ и разбор
+B)Тип доступен по короткому пути крейта, внутренняя раскладка модулей скрыта// разбор: pub use — переэкспорт: элемент становится частью публичного API по новому пути, оставаясь одним и тем же типом. Так строят фасад: пользователи пишут crate::Config, а внутренние модули можно перекладывать, не ломая их код. Обычный use виден только в своём модуле и наружу ничего не добавляет.
- Чем крейт отличается от модуля?A)Крейт — единица компиляции и распространения, модуль — узел дерева внутри негоB)Крейт — это файл, а модуль — папка с файламиC)Крейт — это библиотека, модуль — исполняемая часть проектаD)Крейт в проекте один, модулей сколько угодно
показать ответ и разбор
+A)Крейт — единица компиляции и распространения, модуль — узел дерева внутри него// разбор: Крейт компилируется целиком и целиком публикуется: бинарный (с main) или библиотечный (с lib.rs). Внутри него mod строит дерево имён и границы видимости, и файлы тут вторичны — модуль может лежать в отдельном файле, в папке или прямо в фигурных скобках. В рабочем пространстве крейтов много, каждый собирается отдельно и версионируется сам по себе.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.