Модули, документация, тестване
Модули
Начин да си организираме кода в отделни namespaces.
Обикновенно йерархията от модули съвпада с йерархията на файловете на проекта ни.
Йерархия
Проекта ни съдържа един главен файл, който е и главния модул:
- src/main.rs - ако проекта е изпълнима програма (binary)
- src/lib.rs - ако проекта е библиотека (library)
Типа на проекта се задава, когато изпълним cargo new
или cargo init
:
cargo new project-name --lib
cargo new project-name --bin
cargo new project-name
- по подразбиране се създава binary
При създаване на нов проект получаваме следната йерахия от файлове:
$ cargo new communicator --lib
$ tree communicator
communicator
├── Cargo.toml
└── src
└── lib.rs
Можем да дефинираме подмодули в същия файл.
// src/lib.rs
mod network {
fn connect() { /* ... */ }
}
mod client {
fn init() { /* ... */ }
}
fn main() {} // src/lib.rs mod network { fn connect() { /* ... */ } } mod client { fn init() { /* ... */ } }
Можем да дефинираме подмодули и в отделни файлове
- декларираме подмодулите с
mod MOD_NAME;
- компилатора търси файл
./MOD_NAME.rs
или./MOD_NAME/mod.rs
спрямо текущия файл
communicator
├── Cargo.toml
└── src
├── client.rs
├── lib.rs
└── network.rs
// src/lib.rs
mod network;
mod client;
// src/network.rs
fn connect() { /* ... */ }
// src/client.rs
fn init() { /* ... */ }
Можем да имаме няколко нива на подмодули.
// src/lib.rs
mod network {
fn connect() { /* ... */ }
mod tcp {
/* ... */
}
}
mod network { fn connect() { /* ... */ } mod tcp { /* ... */ } } fn main() {}
Ако искаме да са в отделни файлове трябва да използваме директории
- модула
::network::tcp
е във файлsrc/network/tcp.rs
(технически можем да го сложим и вsrc/network/tcp/mod.rs
, но няма смисъл) - модула
::network
може да е вsrc/network.rs
илиsrc/network/mod.rs
- няма правилно и грешно, и двете се срещат, изберете си конвенция и я следвайте:src/network/mod.rs
- цялото съдържание на модула е видимо на едно място в йерархията (ако използвате някакъв вид file tree view на редактора ви), но ще имате множество файлове mod.rs в проектаsrc/network.rs
- обратното на горното
communicator
├── Cargo.toml
└── src
├── client.rs
├── lib.rs
└── network
├── tcp.rs
└── mod.rs
communicator
├── Cargo.toml
└── src
├── client.rs
├── lib.rs
├── network.rs
└── network
└── tcp.rs
Use
Всяко декларирано нещо (item) - структира, функция, модул, т.н. - си има канонично име.
То започва с ключовата дума crate
, ако е дефинирано в нашия проект
crate::some_module::some_item
Или с името на библиотеката (crate-а), ако е дефинирано извън проекта. Стандартната библиотека (std
) се счита за външна библиотека:
::std::vec::Vec
::external_crate::some_module::some_item
Можем да използавме директно всичко декларирано в текущия модул.
Освен това външните библиотеки са автоматично импортирани и могат да се използват директно, без началните ::
std::vec::Vec
external_crate::some_module::some_item
mod client {
fn load_config() { /* ... */ }
fn init() {
// това е `crate::client::load_config`
load_config();
}
}
mod client { fn load_config() { /* ... */ } fn init() { // това е `crate::client::load_config` load_config(); } } fn main() {}
Неща декларирани в друг модул могат да се използват през пълното канонично име.
Също така може да се използва self::...
или super::...
за релативен път. self
означава текущия модул, super
означава родителския модул (едно ниво нагоре по йерархията).
mod network {
pub fn connect() { /* ... */ }
}
mod client {
fn init() {
crate::network::connect();
// или
super::network::connect();
}
}
fn main() {} mod network { pub fn connect() { /* ... */ } } mod client { fn init() { crate::network::connect(); // или super::network::connect(); } }
С ключовата дума use
се импортират неща от друг модул.
mod network {
pub fn connect() { /* ... */ }
}
mod client {
use crate::network::connect;
// или use super::network::connect;
fn init() {
connect();
}
}
fn main() {} mod network { pub fn connect() { /* ... */ } } mod client { use crate::network::connect; // или use super::network::connect; fn init() { connect(); } }
Могат да се импортират по няколко неща наведнъж - use crate::client::{something, some_other_thing};
Импортираните неща могат да се преименуват - use crate::client::something as some_other_thing;
Може да се импортира всичко от даден модул с glob import - use crate::client::*;
- не се препоръчва, освен в специални случаи (тестове, prelude модули), защото прави кода по-труден за разбиране.
Достъп
В Rust има две нива на достъп - public и private.
Private неща могат да се достъпват само от модула, в който са дефинирани, или от подмодули на него.
Public неща могат да се достъпват и от модули по-нагоре в йерархията.
С други думи енкапсулацията в Rust става на ниво модули. В даден модул имаме директен достъп до всичко останало дефинирано в модула. Това е различно от повечето обектно-ориентирани езици, където енкапсулацията се случва на ниво класове.
Нивото на достъп по подразбиране е private.
mod db {
struct User {
username: String,
email: String,
}
}
use self::db::User;
error[E0603]: struct `User` is private --> src/bin/main_7142f7aa21b52896314d9ff4e7d0f754ca0da8d4.rs:10:15 | 10 | use self::db::User; | ^^^^ private struct | note: the struct `User` is defined here --> src/bin/main_7142f7aa21b52896314d9ff4e7d0f754ca0da8d4.rs:4:5 | 4 | struct User { | ^^^^^^^^^^^ For more information about this error, try `rustc --explain E0603`. error: could not compile `rust` (bin "main_7142f7aa21b52896314d9ff4e7d0f754ca0da8d4") due to 1 previous error
fn main() {} mod db { struct User { username: String, email: String, } } use self::db::User;
За да може да се използва извън db
модула, структурата трябва да се направи публично достъпна с ключовата дума pub
.
mod db {
pub struct User {
// ^^^
username: String,
email: String,
}
}
use self::db::User;
#![allow(unused_imports)] fn main() {} mod db { pub struct User { // ^^^ username: String, email: String, } } use self::db::User;
Декларирана по този начин, структурата User
е публично достъпна, но полетата са все още private. За да може да се достъпват полетата, трябва и те да се декларират като pub
. Ако всички полета са публични, можем да използваме синтаксиса за конструиране на обект литерал от структурата.
mod db {
pub struct User {
pub username: String,
pub email: String,
// ^^^
}
}
use self::db::User;
fn main() {
let user = User {
username: "Иван Иванов".to_string(),
email: "ivan@example.com".to_string(),
};
println!("{}", user.username);
println!("{}", user.email);
}
Иван Иванов ivan@example.com
mod db { pub struct User { pub username: String, pub email: String, // ^^^ } } use self::db::User; fn main() { let user = User { username: "Иван Иванов".to_string(), email: "ivan@example.com".to_string(), }; println!("{}", user.username); println!("{}", user.email); }
Алтернативно, можем да оставим полетата private. Тогава ще трябва да добавим публични методи или функции за работа със структурата.
mod db {
#[derive(Debug)]
pub struct User {
username: String,
email: String,
}
impl User {
pub fn new(username: String, email: String) -> User {
// ^^^
User { username, email }
}
pub fn username(&self) -> &str {
// ^^^
&self.username
}
}
pub fn make_user(username: String, email: String) -> User {
// функцията има достъп до полетата на User, защото е декларирана
// в същия модул
User { username, email }
}
}
use self::db::User;
fn main() {
let user1 = User::new("Иван Иванов".to_string(), "ivan@example.com".to_string());
let user2 = db::make_user("Стоян Стоянов".to_string(), "stoyan@example.com".to_string());
println!("{:?}", user1);
println!("{}", user2.username());
}
User { username: "Иван Иванов", email: "ivan@example.com" } Стоян Стоянов
mod db { #[derive(Debug)] pub struct User { username: String, email: String, } impl User { pub fn new(username: String, email: String) -> User { // ^^^ User { username, email } } pub fn username(&self) -> &str { // ^^^ &self.username } } pub fn make_user(username: String, email: String) -> User { // функцията има достъп до полетата на User, защото е декларирана // в същия модул User { username, email } } } use self::db::User; fn main() { let user1 = User::new("Иван Иванов".to_string(), "ivan@example.com".to_string()); let user2 = db::make_user("Стоян Стоянов".to_string(), "stoyan@example.com".to_string()); println!("{:?}", user1); println!("{}", user2.username()); }
Конвенцията за името на getter функцията за поле foo
е:
foo()
за read-only getter - връща&T
foo_mut()
за read-write getter - връща&mut T
set_foo()
за setter - приемаT
Забележете, че няма проблем да имаме поле и метод с едно и също име.
структури, енуми и други
Полетата на структурите са private по подразбиране и трябва да се означат с pub
, за да се направят публични.
Полетата на структурите-кортежи (tupple structs) също са private по подразбиране
Вариантите на enum винаги са публични, но самия enum може да се дефинира като public или private.
Функции и асоциирани типове в trait също са публични, но самия trait може да се дефинира като public или private.
// съдържанието на токена не може да се достъпи от външния свят
pub struct SecretToken(u32);
// съдържанието на токена може да се достъпва или променя свободно
pub struct PublicToken(pub u32);
// енума може да се достъпва само от текущия модул
enum Foo { Variant1, Variant2(String) }
// енума може да се достъпва извън текущия модул.
// Всички варианти и стойностите на тези варианти са публично достъпни
pub enum Bar { Variant1, Variant2(String) }
pub use
use
създава ново име, което е private за текущия модул.
pub use
прави същото, но името е публично видимо извън модула. Това се нарича reexport и често се използва за предоставяне на достъп до предмети, които биха били private иначе.
mod module {
pub use submodule::A;
use submodule::B;
// private подмодул
mod submodule {
pub struct A {}
pub struct B {}
}
}
use module::A; // ok
// use module::submodule::A; // грешка - `submodule` is private
// use module::B; // грешка - `B` is private
// use module::submodule::B; // грешка - `submodule` private
#![allow(unused_imports)] fn main() {} mod module { pub use submodule::A; use submodule::B; // private подмодул mod submodule { pub struct A {} pub struct B {} } } use module::A; // ok // use module::submodule::A; // грешка - `submodule` is private // use module::B; // грешка - `B` is private // use module::submodule::B; // грешка - `submodule` private
pub(crate)
Навсякъде, където можем да напишем pub
, можем да напишем и pub(crate)
, pub(super)
или pub(in some::path)
.
Тези варианти добавят допълнителни ограничения откъде може да се достъпва даденото нещо.
pub(crate)
работи като pub
в рамките на текущата библиотека (crate), но не позволява името да се достъпва извън това - от проекти, които използват текущия crate като външна библиотека.
pub(super)
и pub(in some::path)
ограничават видимостта до определен модул и на практика почти никога не се използват.
Тестове
TODO