Модули и видимость в Rust
В Rust всё приватно, пока не написан pub. Модуль работает как граница инкапсуляции: pub открывает элемент наружу совсем, pub(crate) — только внутри своего крейта. А pub use переэкспортирует то, что лежит глубоко, и позволяет собрать удобный публичный API фасадом, не раскрывая внутреннюю структуру.
Вопросы про модули выглядят скучно, но отвечают на них плохо. Спрашивают, что видно по умолчанию, чем 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 строит дерево имён и границы видимости, и файлы тут вторичны — модуль может лежать в отдельном файле, в папке или прямо в фигурных скобках. В рабочем пространстве крейтов много, каждый собирается отдельно и версионируется сам по себе.
дальше
Теорию прочитал. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.