Aprende Rust

Lección 20 de 26 Genéricos, traits y módulos

Módulos, crates y Cargo

  • 5 min de lectura
  • 3 ejercicios
  1. Un crate es lo que compila Rust de una vez. Dentro, mod agrupa el código en módulos, como carpetas.
  2. Todo es privado por defecto. Con pub abres la puerta: ingresar se usa desde fuera, registrar no.
  3. Las rutas usan ::. Con use creas un atajo: en vez de formas::circulo::area, escribes circulo::area.
  4. El código de otros llega de crates.io: cargo add rand lo apunta en Cargo.toml y Cargo lo descarga.
1/4

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 rustc de 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 main y genera un ejecutable. Su raíz es src/main.rs.
  • Biblioteca: no tiene main ni se ejecuta sola; ofrece código para que lo usen otros. Su raíz es src/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 biblioteca
mi_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 derive

La 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.rs en lugar de src/tienda.rs. Funciona igual, pero hoy se recomienda tienda.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

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.
Salida esperada
42
25
src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos