Модули
Во избежание конфликтов имён, а также для логического структурирования кода в программах на Rust принято группировать типы данных и функции в модули.
Tip
Модули в какой-то мере являются аналогами пространств имен из C++ или пакетов из Java.
Имеется несколько способов создать модуль.
Способ 1: блок mod { … }
Модуль можно объявить прямо в main.rs при помощи ключевого слова mod.
mod имя_модуля {
содержимое
}
Далее, чтобы обратиться к функции или структуре из модуля, используется синтаксис:
модуль::имя
Рассмотрим пример: решения конфликта имён при помощи модулей.
Файл main.rs:
mod a {
pub fn get_num() -> i32 {
1
}
}
mod b {
pub fn get_num() -> i32 {
2
}
}
fn main() {
println!("{}", a::get_num()); // 1
println!("{}", b::get_num()); // 2
}
Обратите внимание, что перед объявлением функции стоит модификатор видимости pub (public). Он указывает, что функция может быть использована “внешним” кодом. По умолчанию всё содержимое модуля является приватным и может быть использовано только внутри этого модуля.
mod my_module {
pub fn get_num() -> i32 { // видима за пределами модуля
get_5()
}
fn get_5() -> i32 { // видима только внутри модуля
5
}
}
fn main() {
println!("{}", my_module::get_num()); // 5
}
Как правило, блок mod { ... } используют:
- для решения конфликта имён
- для вынесения части кода в блок (модуль), который компилируется только при выполнении некого условия
Способ 2: другой *.rs файл
Если в той же директории, где находится файл main.rs, создать другой файл с расширением *.rs, то этот файл можно будет подключить как модуль при помощи ключевого слова mod.
mod имя_файла_без_rs;
Например, допустим, у нас имеются такие файлы:
main.rs:mod my_module; fn main() { println!("{}", my_module::get_num()); }my_module.rs:#![allow(unused)] fn main() { pub fn get_num() -> i32 { 5 } }
Скомпилируем и запустим
$ rustc main.rs
$ ./main
5
Note
Сильно упрощая, можно сказать, что при компиляции файла
main.rsвместо выраженияmod my_moduleпомещается содержимое файлаmy_module.rsтак, как если бы оно изначально находилось там и было просто заключено в блокmod my_module { ... }.mod my_module { pub fn get_num() -> i32 { 5 } } fn main() { println!("{}", my_module::get_num()); }
Чтобы не писать полное имя функции с указанием модуля, её можно импортировать при помощи директивы use.
mod my_module;
use my_module::get_num;
fn main() {
println!("{}", get_num());
}
Способ создания модуля в виде отдельного *.rs файла подходит для случаев, когда весь код модуля можно поместить в один файл.
Способ 3: директория и mod.rs
Если функциональность модуля хорошо гранулирована и может быть разделена на несколько подмодулей, то модуль создают не как отдельный файл, а как отдельный каталог, в котором есть корневой файл модуля — mod.rs и другие *.rs файлы, которые служат в качестве под-модулей.
Для примера рассмотрим тривиальный модуль с арифметическими операциями.
src/
├── main.rs <- главный файл программы
└── my_math/ <- папка модуля
└── mod.rs <- корневой файл модуля
├── add.rs <- здесь функция сложения
└── mul.rs <- здесь функция умножения
Файлы программы:
-
Файл
my_math/mod.rs:pub mod add; // подключаем add.rs в модуль my_math pub mod mul; // подключаем mul.rs в модуль my_math -
Файл
my_math/add.rs:pub fn sum(a: i32, b: i32) -> i32 { a + b } -
Файл
my_math/add.rs:pub fn multiply(a: i32, b: i32) -> i32 { a * b } -
Файл
main.rs:mod my_math; use my_math::add::sum; use my_math::mul::multiply; fn main() { println!("2 + 3 = {}", sum(2, 3)); println!("2 * 3 = {}", multiply(2, 3)); }
Скомпилируем и запустим программу:
$ rustc main.rs
$ ./main
7
Note
Упрощая, можно сказать, что при компиляции
main.rs, компилятор также соберёт все файлы модулей воедино, словно изначально это был файл:mod my_math { pub mod add { pub fn sum(a: i32, b: i32) -> i32 { a + b } } pub mod mul { pub fn multiply(a: i32, b: i32) -> i32 { a + b } } } use my_math::add::sum; use my_math::mul::multiply; fn main() { println!("2 + 3 = {}", sum(2, 3)); println!("2 * 3 = {}", multiply(2, 3)); }
Трансляция и модули
Как мы уже сказали, когда мы создаём модули в виде отдельных файлов, нам не приходится компилировать их отдельно. Мы вызываем rustc только для файла main.rs, и все модули компилируются автоматически.
Сборку бинарного исполняемого файла можно проиллюстрировать так:
Для сравнения: в C++ каждый *.cpp файл компилируется в отдельный объектный файл, и только потом все объектные файлы линкуются в исполняемый бинарный файл.
Из-за того, что компилятор Rust “склеивает” main.rs и все входящие в него модули в один большой файл, который далее компилирует целиком, этот самый main.rs и все входящие в него модули называют крэйтом (crate — ящик). Представьте, словно все файлы “свалили” в один ящик (единицу трансляции) и подали на вход компилятору.
Понятие крэйта очень важно в экосистеме Rust. Мы подробнее поговорим про крэйты, и другие составляющие программ на Rust в главе Несколько исполняемых файлов.