Lección 20 de 26 Genéricos, traits y módulos
Módulos, crates y Cargo
- Un crate es lo que compila Rust de una vez. Dentro, mod agrupa el código en módulos, como carpetas.
- Todo es privado por defecto. Con pub abres la puerta: ingresar se usa desde fuera, registrar no.
- Las rutas usan ::. Con use creas un atajo: en vez de formas::circulo::area, escribes circulo::area.
- El código de otros llega de crates.io: cargo add rand lo apunta en Cargo.toml y Cargo lo descarga.
Mientras tu programa cabe en un fichero, todo va bien. Pero cuando crece necesitas organizarlo: agrupar el código relacionado, decidir qué partes se ven desde fuera y reutilizar código escrito por otras personas. Rust lo resuelve con su sistema de módulos y con Cargo.
Hay cuatro piezas, de mayor a menor:
- Paquete: un proyecto de Cargo, con su
Cargo.toml. - Crate: lo que compila
rustcde una vez; un ejecutable o una biblioteca. - Módulo: una forma de organizar el código dentro de un crate y controlar qué es visible.
- Ruta: el nombre completo de un elemento, como
std::collections::HashMap.
Paquetes y crates
Un crate es la unidad mínima que compila Rust. Hay dos tipos:
- Binario: tiene una función
mainy genera un ejecutable. Su raíz essrc/main.rs. - Biblioteca: no tiene
mainni se ejecuta sola; ofrece código para que lo usen otros. Su raíz essrc/lib.rs.
Un paquete agrupa uno o varios crates: como mucho una biblioteca y tantos binarios como quieras.
cargo new mi_app # paquete con un crate binario
cargo new --lib utilidades # paquete con un crate bibliotecami_app/
├── Cargo.toml
├── Cargo.lock
└── src/
├── main.rs ← raíz del crate binario
└── lib.rs ← raíz del crate biblioteca (opcional)Cargo.toml y dependencias
Cargo.toml describe el paquete: nombre, versión, edición de Rust y dependencias. Las dependencias son crates de otras personas, publicados en crates.io, el registro oficial de Rust.
[package]
name = "mi_app"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.9"
serde = { version = "1", features = ["derive"] }No hace falta editarlo a mano: cargo add busca la última versión y añade la línea por ti.
cargo add rand
cargo add serde --features deriveLa próxima vez que compiles, Cargo descarga y compila las dependencias. En tu código las usas con su nombre como primera parte de la ruta:
use rand::Rng;
fn tirar_dado() -> u8 {
rand::rng().random_range(1..=6)
}rand = "0.9" no fija una versión exacta: acepta cualquier versión compatible (0.9.0, 0.9.2…), siguiendo el versionado semántico. La versión exacta que se usa queda apuntada en Cargo.lock, que Cargo genera la primera vez que compilas. Así todo el mundo compila con las mismas versiones hasta que ejecutes cargo update. No lo edites a mano y, en aplicaciones, súbelo al repositorio.
Módulos con mod
Dentro de un crate, mod agrupa código relacionado en un módulo. Puedes escribirlo con llaves en el mismo fichero:
mod saludos {
pub fn hola() {
println!("¡Hola!");
}
pub mod formal {
pub fn hola() {
println!("Buenos días, ¿cómo está usted?");
}
}
}
fn main() {
saludos::hola();
saludos::formal::hola();
}Los módulos se pueden anidar y forman un árbol cuya raíz es el propio crate, llamada crate. Fíjate en que hay dos funciones hola sin conflicto: viven en módulos distintos.
Privacidad: todo es privado por defecto
Todo lo que hay dentro de un módulo (funciones, structs, constantes, submódulos…) es privado por defecto: solo se puede usar desde ese módulo y sus descendientes. Para que se vea desde fuera, márcalo con pub:
mod banco {
pub fn ingresar(cantidad: u32) {
registrar(cantidad);
}
fn registrar(cantidad: u32) {
println!("Registrado: {cantidad} €");
}
}
fn main() {
banco::ingresar(100); // bien: es pública
banco::registrar(100); // error: la función es privada
}Gracias a esto puedes cambiar los detalles internos de un módulo, como registrar, sin romper el código que lo usa. Solo lo que es pub forma parte de su «contrato» con el exterior.
pub en structs y enums
Hacer público un struct no hace públicos sus campos: cada campo decide por sí mismo. Es muy útil para proteger datos que deben cumplir una regla:
mod usuarios {
pub struct Usuario {
pub nombre: String,
edad: u32, // privado: solo se toca desde este módulo
}
impl Usuario {
pub fn new(nombre: &str, edad: u32) -> Usuario {
Usuario { nombre: String::from(nombre), edad: edad.min(150) }
}
pub fn edad(&self) -> u32 {
self.edad
}
}
}
fn main() {
let u = usuarios::Usuario::new("Ana", 200);
println!("{} tiene {} años", u.nombre, u.edad());
// u.edad = 30; // error: el campo es privado
}Como edad es privado, desde fuera no puedes crear un Usuario escribiendo sus campos entre llaves: necesitas un constructor público como new. Así nadie puede saltarse su regla (una edad de 150 como máximo). Con los enums es al revés: si el enum es pub, todas sus variantes lo son.
Rutas: crate, self y super
Para nombrar un elemento escribes su ruta, separando cada nivel con ::. Puede ser:
- Absoluta: empieza en la raíz con
crate::. - Relativa: empieza en el módulo actual. Con
self::(el módulo actual),super::(el módulo padre) o directamente con un nombre.
mod cocina {
pub fn preparar() {
self::hornear(); // en este mismo módulo
super::servir("pizza"); // en el módulo padre (la raíz)
}
fn hornear() {
println!("Horneando...");
}
}
fn servir(plato: &str) {
println!("¡Marchando una {plato}!");
}
fn main() {
crate::cocina::preparar(); // ruta absoluta
cocina::preparar(); // ruta relativa
}use: rutas más cortas
Repetir rutas largas cansa. use crea un atajo en el ámbito actual:
mod formas {
pub mod circulo {
pub fn area(radio: f64) -> f64 {
std::f64::consts::PI * radio * radio
}
}
}
use formas::circulo;
use std::collections::HashMap;
fn main() {
let mut areas = HashMap::new();
areas.insert("pequeño", circulo::area(1.0));
areas.insert("grande", circulo::area(10.0));
println!("{:.2}", areas["grande"]);
}La convención: para funciones, importa el módulo padre (circulo::area(...)), así se ve de dónde viene; para structs, enums y traits, importa el elemento completo (HashMap). Puedes agrupar varios use con llaves: use std::collections::{HashMap, HashSet};.
Si dos elementos se llaman igual, renombra uno con as:
use std::fmt::Result;
use std::io::Result as IoResult;Módulos en ficheros
En proyectos reales cada módulo vive en su propio fichero. Si escribes mod tienda; con punto y coma en lugar de llaves, le dices al compilador: «el contenido de este módulo está en otro fichero». Lo buscará en src/tienda.rs. Y si dentro de tienda.rs escribes pub mod carrito;, lo buscará en src/tienda/carrito.rs.
src/
├── main.rs ← mod tienda;
├── tienda.rs ← pub mod carrito;
└── tienda/
└── carrito.rs// src/tienda/carrito.rs
pub struct Carrito {
pub productos: Vec<String>,
}// src/main.rs
mod tienda;
use tienda::carrito::Carrito;El árbol de módulos es el mismo que si lo hubieras escrito todo con llaves en main.rs: solo cambia dónde está el texto. Ojo: mod se declara una sola vez, en el módulo padre. No es un import: para usar el módulo desde otros sitios, usa use.
Estilo antiguo: En proyectos más viejos verás
src/tienda/mod.rsen lugar desrc/tienda.rs. Funciona igual, pero hoy se recomiendatienda.rs.
Re-exportar con pub use
Un use normal crea un atajo privado. Con pub use el atajo también es público: re-exportas el elemento en otra ruta. Las bibliotecas lo usan para ofrecer una API sencilla aunque por dentro tengan muchos módulos:
mod tienda {
pub mod inventario {
pub mod frutas {
pub fn precio(fruta: &str) -> u32 {
match fruta {
"manzana" => 50,
"pera" => 70,
_ => 100,
}
}
}
}
pub use inventario::frutas::precio;
}
fn main() {
// Sin pub use tendrías que escribir tienda::inventario::frutas::precio
println!("{} céntimos", tienda::precio("pera"));
}Hora de practicar
Ejercicios
0/3 resueltos
Reto 1 de 3 Resuelto
main no compila porque intenta usar elementos privados del módulo matematicas. Añade pub solo donde haga falta para que pueda llamar a doble y a geometria::area_cuadrado.42 25
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.