Guía de Rust: De 0 a 100
¿Qué es Rust?
Rust es un lenguaje compilado, tipado estáticamente, enfocado en seguridad de memoria sin GC, concurrencia sin carreras de datos y rendimiento C/C++. Creado por Graydon Hoare en 2006 en Mozilla Research, lanzado públicamente en 2010, estable desde 2015.
¿Qué lo hace único?
- Ownership: el compilador verifica en compile-time que no haya use-after-free, double free o data races. Sin GC ni gestión manual.
- Zero-cost abstractions: las abstracciones se compilan a código máquina tan eficiente como el escrito a mano.
- Fearless concurrency: el borrow checker previene data races en compilación.
- FFI sin runtime: integrable en cualquier runtime (embebido, kernel, WASM).
¿Dónde se usa?
- Sistemas: Linux kernel (Rust-for-Linux 6.1+), Fuchsia OS
- Navegadores: Firefox (Stylo, WebRender), Servo
- Cloud: AWS (Firecracker VMM, Lambda runtime), Cloudflare (Pingora), Dropbox (filesystem)
- CLI/DevTools: ripgrep, fd, bat, delta, zoxide, alacritty, starship
- WebAssembly: Figma, Shopify, Atlassian
- Blockchain: Solana, Polkadot, Near
- Embebido: Tock OS, Embassy, ESP-RS
¿Quién lo usa? Microsoft (Azure IoT), Google (Android/Fuchsia), Meta, Amazon, Discord, Figma.
Fuente: Rust官方, Rust Blog, Rust in Android
Prerrequisitos
Antes de empezar, necesitas:
- Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
- El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)
Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.
Instalación
rustup (todas las plataformas)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
rustc --version # rustc 1.85.0 (2026-02-20)
cargo --version
Esto instala: rustup (gestor de versiones), rustc (compilador), cargo (gestor de paquetes/build).
Windows
- Ejecuta
rustup-init.exedesde rustup.rs - Necesita Visual Studio Build Tools con workload “Desktop C++” (o MSYS2/MinGW)
- Alternativa:
winget install Rustlang.Rustup
macOS
xcode-select --install # Xcode CLT necesario para linker
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Linux (Debian/Ubuntu)
curl ... | sh
sudo apt install build-essential pkg-config libssl-dev
Fedora: sudo dnf install gcc pkg-config openssl-devel
rustup avanzado
rustup update # Actualizar
rustup install nightly # Canal nightly
rustup target add wasm32-unknown-unknown # Cross-compile WASM
rustup component add rust-src # Para rust-analyzer
rustup component add rustfmt clippy
rustup show # Toolchains instaladas
rustup default nightly # Cambiar default
Docker multi-stage
FROM rust:1.85-slim-bookworm AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release 2>/dev/null || true
COPY src/ src/
RUN cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/mi-app /usr/local/bin/
CMD ["mi-app"]
Alias útiles
alias rlb='cargo build --release'
alias rchk='cargo check'
alias rtest='cargo test'
alias rfmt='cargo fmt'
alias rclp='cargo clippy'
alias rrun='cargo run --'
alias radd='cargo add'
alias rdoc='cargo doc --open'
alias rclean='cargo clean'
alias rtree='cargo tree'
alias rbench='cargo bench'
alias ro='cargo outdated'
alias rup='cargo upgrade'
Fuente: rustup.rs, Docker Rust
Escala de aprendizaje 0–100 (7 niveles)
Nivel 0–15: Fundamentos
Conceptos: sintaxis básica, variables, tipos escalares, if/else, loops, funciones, cargo new, cargo run.
Proyecto: calculadora CLI — suma, resta, multiplica, divide con manejo de errores.
use std::io;
fn main() -> Result<(), String> {
let mut input = String::new();
println!("Primer número:");
io::stdin().read_line(&mut input).unwrap();
let a: f64 = input.trim().parse().map_err(|_| "número inválido")?;
input.clear();
println!("Operación (+, -, *, /):");
io::stdin().read_line(&mut input).unwrap();
let op = input.trim().to_string();
input.clear();
println!("Segundo número:");
io::stdin().read_line(&mut input).unwrap();
let b: f64 = input.trim().parse().map_err(|_| "número inválido")?;
let result = match op.as_str() {
"+" => a + b,
"-" => a - b,
"*" => a * b,
"/" => if b != 0.0 { a / b } else { return Err("división por cero".into()) },
_ => return Err("operación inválida".into()),
};
println!("Resultado: {result}");
Ok(())
}
Nivel 15–30: Structs, enums, traits
Conceptos: structs, enums, pattern matching, traits, Option/Result, Vec, String.
Proyecto: gestor de tareas (TODO list) en memoria con persistencia JSON.
Nivel 30–45: Smart pointers e iterators
Conceptos: Box, Rc, Arc, closures, iterators (map, filter, fold), colecciones (HashMap, HashSet).
Proyecto: procesador de logs — lee archivo, parsea líneas, cuenta ocurrencias por nivel (INFO, WARN, ERROR), genera reporte.
Nivel 45–60: Testing, documentación y módulos
Conceptos: tests unitarios/de integración, doc tests, módulos, visibilidad, clippy, rustfmt.
Proyecto: librería de validación — funciones validadoras con tests exhaustivos, documentación, CI configurado.
Nivel 60–75: Web APIs asíncronas
Conceptos: async/await, Tokio, Axum/Actix, serialización JSON, SQLx/Diesel, WebSockets.
Proyecto: API REST de URLs acortadas — endpoints CRUD, BD PostgreSQL, tests de integración, Docker.
Nivel 75–90: Producción
Conceptos: release profiles (LTO, codegen-units), Docker multi-stage, CI/CD (GitHub Actions), logging (tracing), benchmarks (criterion), profiling (perf, flamegraph).
Proyecto: el proyecto del nivel anterior con CI/CD, Docker, benchmarks, profiling y monitoreo.
Nivel 90–100: Sistemas y arquitectura
Conceptos: unsafe, FFI, no_std, WASM, Pin, async traits, arquitectura hexagonal, patrones avanzados, sistemas distribuidos.
Proyecto: intérprete de un lenguaje pequeño (AST, lexer, parser, evaluador) — compila a WASM.
Primeros pasos y configuración del entorno
Si prefieres ver directamente el “Hola Mundo” en Rust, consulta El Atlas del Hola Mundo.
Crear proyecto
cargo new mi-proyecto
cd mi-proyecto
Estructura:
mi-proyecto/
├── Cargo.toml # Manifiesto
└── src/
└── main.rs # Punto de entrada
Cargo.toml
[package]
name = "mi-proyecto"
version = "0.1.0"
edition = "2021" # 2015, 2018, 2021, 2024
[dependencies]
# serde = { version = "1", features = ["derive"] }
# tokio = { version = "1", features = ["full"] }
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = true
Compilar y ejecutar
cargo build # Debug
cargo build --release # Optimizado
cargo run # Compila + ejecuta
cargo check # Solo verifica tipos (más rápido)
cargo run -- arg1 arg2
rustfmt
rustup component add rustfmt
cargo fmt # Formatea
cargo fmt --check # Verifica (CI)
Config rustfmt.toml:
max_width = 100
tab_spaces = 4
edition = "2021"
Clippy
rustup component add clippy
cargo clippy # Lints
cargo clippy -- -W clippy::pedantic # Pedante
cargo clippy --fix # Auto-fix
Editores recomendados
Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.
Ediciones de Rust
| Edition | Rust | Novedades |
|---|---|---|
| 2015 | 1.0 | Original |
| 2018 | 1.31 | impl Trait, dyn Trait, NLL, ?, async/await, módulos modernos |
| 2021 | 1.56 | IntoIterator for arrays, TryInto closures, bindings_after_at |
| 2024 | 1.85 | impl Trait everywhere, unsafe extern blocks, cargo script, gen blocks, RPITIT |
Migración: cargo fix --edition
Fuente: The Rust Book — Getting Started, Edition Guide
Paradigmas de programación
Rust es un lenguaje multi-paradigma con énfasis en funcional y sistémico. Puedes mezclar paradigmas según el problema.
| Paradigma | Soporte en Rust |
|---|---|
| Imperativo | Statements, loops, mutabilidad explícita |
| Funcional | Inmutabilidad, iterators, closures, pattern matching, Option/Result |
| POO | Structs + impl, traits, composición, encapsulación |
| Concurrente | Ownership + Send/Sync, canales, Arc<Mutex> |
| Sistémico | Sin GC, control de memoria, zero-cost, FFI, no_std |
Imperativo
Rust tiene statements y expressions. Casi todo es una expresión (retorna un valor):
let x = if cond { 10 } else { 20 }; // if es expresión
let y = loop { break 42; }; // loop retorna valor
let z = match n {
1 => "uno",
_ => "otro",
};
Funcional
Rust adopta varios conceptos funcionales:
- Inmutabilidad por defecto:
letcrea variables inmutables. Sololet mutpermite mutación. - Iterators: lazy, zero-cost, chainable (
map,filter,fold,flat_map) - Closures:
|args| expr, capturan entorno por referencia/move, traitsFn/FnMut/FnOnce - Pattern matching exhaustivo:
matchobliga a cubrir todos los casos - Option/Result como mónadas:
map,and_then,or_else,unwrap_or_elsepermiten encadenar sin anidar
let pares: Vec<i32> = (0..10)
.filter(|x| x % 2 == 0)
.map(|x| x * 2)
.collect();
let r = Some(10)
.map(|x| x * 2)
.filter(|x| x > 5)
.and_then(|x| if x < 100 { Some(x) } else { None });
POO parcial
Rust no tiene clases ni herencia, pero ofrece:
- Structs +
impl: datos + métodos (como clases sin herencia) - Traits como interfaces: definen comportamiento compartido
- Composición sobre herencia: agregar campos en lugar de extender
- No clases: no hay
class,extends,super,protected - Encapsulación:
pubcontrola visibilidad de campos y métodos
trait Volador { fn volar(&self); }
struct Pajaro { nombre: String }
struct Avion { modelo: String }
impl Volador for Pajaro { fn volar(&self) { println!("{} bate alas", self.nombre); } }
impl Volador for Avion { fn volar(&self) { println!("{} enciende motores", self.modelo); } }
Concurrente
El ownership garantiza thread-safety en compilación:
- El borrow checker previene data races: no puedes tener
&mutsimultáneos entre threads - Traits
Send/Sync: el compilador verifica qué tipos son seguros entre threads Rc<T>no esSend(contador no atómico),Arc<T>síMutex<T>protege acceso concurrente con RAII
use std::sync::{Arc, Mutex};
use std::thread;
let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&contador);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("{}", contador.lock().unwrap()); // 10
Sistémico
Rust da control de memoria sin GC:
- Sin garbage collector: la memoria se libera en compile-time con ownership
- Zero-cost abstractions: iterators, closures, genéricos se compilan a código tan eficiente como el escrito a mano
- Control manual: puedes elegir stack vs heap (
Box), memoria compartida (Rc/Arc), representaciones conrepr(C) - FFI sin runtime: integrable en C, Python, JS, WASM, kernel
// Zero-cost: este código se compila a un loop óptimo
let suma: u32 = (0..1000).filter(|x| x % 2 == 0).map(|x| x * x).sum();
Fuente: Rust Book — FP, Rust Book — OOP, Rust Book — Concurrency, Rustonomicon
Tipos de datos y variables
Sistema de tipos
¿Qué es?
Rust tiene tipado estático fuerte con inferencia: el compilador conoce el tipo de cada variable en compile-time y no realiza conversiones implícitas entre tipos incompatibles. Es como un molde rígido: cada pieza encaja exactamente en su lugar.
El sistema de tipos es nominal (los tipos se identifican por su nombre, no por su estructura) y lineal (cada valor tiene exactamente un propietario).
🔗 Wikipedia — Tipado estático, Rust Book — Tipos
Sintaxis básica
let x: i32 = 42; // Anotación explícita
let y = 42; // Inferencia: i32 por defecto
let z = 3.14; // Inferencia: f64 por defecto
// Error: tipos no coinciden
// let suma = "42" + 1; // ❌ No compila
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_tipado_estatico() {
let x: i32 = 42;
assert_eq!(x, 42);
}
#[test]
fn test_inferencia() {
let x = 42;
let y = 3.14;
assert_eq!(std::mem::size_of_val(&x), 4); // i32 = 4 bytes
assert_eq!(std::mem::size_of_val(&y), 8); // f64 = 8 bytes
}
}
💡 Memoria y rendimiento
- Coste cero en runtime: el tipado se resuelve en compilación
- Los tipos ocupan exactamente el tamaño declarado (sin overhead de cabecera)
- La inferencia no añade tiempo de compilación significativo
✅ Buenas prácticas
- ✅ Prefiere inferencia de tipos para código local y legible
- ✅ Anota tipos explícitamente en APIs públicas y funciones
- ⚠️ No uses
i8/u8para números pequeños sin necesidad real de ahorro de memoria - ❌ No abuses de
aspara conversiones — prefiereFrom/TryFrom
🏗️ Metodología
- En bibliotecas: siempre anota tipos en la interfaz pública
- En scripts/CLIs: inferencia es aceptable
- En embebido: usa tipos explícitos para controlar el tamaño exacto
🔗 Para saber más
Escalares
¿Qué es?
Los tipos escalares representan valores simples y únicos. En Rust, todos los escalares tienen tamaño fijo conocido en compilación y viven en el stack.
| Tipo | Tamaño | Rango |
|---|---|---|
i8/u8 | 8 bits | -128..127 / 0..255 |
i16/u16 | 16 bits | -32768..32767 / 0..65535 |
i32/u32 | 32 bits | -2^31..2^31-1 / 0..2^32-1 |
i64/u64 | 64 bits | -2^63..2^63-1 / 0..2^64-1 |
i128/u128 | 128 bits | -2^127..2^127-1 / 0..2^128-1 |
isize/usize | arq. nativa | depende de la arquitectura |
f32/f64 | 32/64 bits | IEEE 754 |
bool | 8 bits | true / false |
char | 32 bits | Unicode escalar |
Sintaxis básica
let decimal = 98_222;
let hex = 0xff;
let octal = 0o77;
let binario = 0b1111_0000;
let byte = b'A';
let flotante = 3.14;
let cientifico = 1.5e10;
let activo: bool = true;
let letra: char = 'ñ';
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_escalares() {
assert_eq!(98_222, 98222);
assert_eq!(0xff, 255);
assert_eq!(1.5e10 as u64, 15_000_000_000);
assert_eq!(b'A', 65u8);
}
#[test]
fn test_tamanos() {
assert_eq!(std::mem::size_of::<i32>(), 4);
assert_eq!(std::mem::size_of::<f64>(), 8);
assert_eq!(std::mem::size_of::<char>(), 4);
assert_eq!(std::mem::size_of::<bool>(), 1);
}
}
💡 Memoria y rendimiento
- Los escalares viven en el stack: acceso O(1), sin asignación dinámica
charocupa 4 bytes (no 1 como en C) para soportar Unicode completoboolocupa 1 byte completo (no 1 bit) — es un trade-off de acceso a memoria
✅ Buenas prácticas
- ✅ Usa
i32/f64por defecto como tipos escalares principales - ✅ Usa
usizepara índices de colecciones y tamaños - ⚠️
isize/usizevarían según plataforma — no los serialices sin anotar el tamaño - ❌ No uses
aspara truncar valores sin verificar el rango
🏗️ Metodología
- Por defecto:
i32para enteros,f64para flotantes - Rendimiento extremo: usa
i8-i64según el rango real esperado - Embebido: prefiere
u8/i16para ahorrar registros y memoria
🔗 Para saber más
Compuestos
¿Qué es?
Los tipos compuestos agrupan múltiples valores en una sola entidad. Rust ofrece tuplas (producto de tipos) y arrays (colección homogénea de tamaño fijo). Ambos tienen tamaño conocido en compilación y viven en el stack.
Sintaxis básica
// Tupla: grupo heterogéneo de tamaño fijo
let persona: (&str, u8, bool) = ("Alice", 30, true);
let (nombre, edad, activo) = persona; // destructuring
println!("{}", persona.0); // acceso por índice
// Array: grupo homogéneo de tamaño fijo
let numeros: [i32; 5] = [1, 2, 3, 4, 5];
let ceros = [0; 100]; // [0, 0, 0, ..., 0]
println!("{}", numeros[2]); // acceso por índice
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_tuplas() {
let t = (42, "hola");
assert_eq!(t.0, 42);
assert_eq!(t.1, "hola");
let (a, b) = t;
assert_eq!(a, 42);
}
#[test]
fn test_arrays() {
let arr = [1, 2, 3];
assert_eq!(arr.len(), 3);
assert_eq!(arr[1], 2);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_array_out_of_bounds() {
let arr = [1, 2, 3];
let _ = arr[10]; // panic en runtime
}
}
💡 Memoria y rendimiento
- Tuplas y arrays viven en el stack: acceso directo, cero overhead
- El tamaño es parte del tipo:
[i32; 5]y[i32; 6]son tipos distintos - Arrays se almacenan contiguamente en memoria (cache-friendly)
- Tuplas pueden tener padding por alineación entre campos
✅ Buenas prácticas
- ✅ Usa arrays para colecciones de tamaño conocido en compilación
- ✅ Usa tuplas para agrupar valores sin nombre (retorno múltiple)
- ⚠️ Para colecciones dinámicas, prefiere
Vec<T>sobre arrays - ❌ No accedas a arrays con índices sin verificar el límite
🏗️ Metodología
- Tuplas: útiles para retornar múltiples valores sin crear un struct
- Arrays: usa para buffers de tamaño fijo, lookup tables, datos embebidos
- Cuando el tamaño es dinámico, refactoriza a
Vec<T>
🔗 Para saber más
Structs
¿Qué es?
Un struct es un tipo compuesto con campos nombrados. Es la unidad fundamental de organización de datos en Rust, equivalente a las clases en otros lenguajes pero sin herencia.
Sintaxis básica
struct Usuario {
nombre: String,
email: String,
activo: bool,
edad: u8,
}
// Instanciación
let u = Usuario {
nombre: String::from("Alice"),
email: String::from("alice@example.com"),
activo: true,
edad: 30,
};
// Tuple struct (campos sin nombre)
struct Color(u8, u8, u8);
let rojo = Color(255, 0, 0);
// Unit struct (sin campos)
struct Marker;
// Spread syntax
let u2 = Usuario { nombre: String::from("Bob"), ..u };
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
struct Punto { x: i32, y: i32 }
#[test]
fn test_struct() {
let p = Punto { x: 10, y: 20 };
assert_eq!(p.x, 10);
assert_eq!(p.y, 20);
}
#[test]
fn test_mutabilidad() {
let mut p = Punto { x: 0, y: 0 };
p.x = 5;
assert_eq!(p.x, 5);
}
}
💡 Memoria y rendimiento
- Los structs se almacenan en stack por defecto, sin overhead
- El tamaño es la suma de sus campos más padding por alineación
#[repr(C)]cambia el layout para FFI (compatible con C)#[repr(packed)]elimina padding pero penaliza acceso
✅ Buenas prácticas
- ✅ Usa
implblocks para asociar métodos al struct - ✅ Implementa
DisplayyDebugpara logging y depuración - ⚠️ No expongas campos privados sin getters si necesitas invariantes
- ❌ No abuses de
puben campos — prefiere métodos de acceso
🏗️ Metodología
- Structs simples: datos planos sin validación (DTOs)
- Structs con lógica: usa constructores (
new()) que validen invariantes - Para configuración: usa el builder pattern con structs
🔗 Para saber más
Enums
¿Qué es?
Los enum en Rust son algebraicos: cada variante puede llevar datos asociados. Son la herramienta principal para modelar estados y opciones, y combinados con match proporcionan exhaustividad en compilación.
Sintaxis básica
enum Estado {
Activo,
Inactivo,
Pendiente(String), // con datos
}
enum Mensaje {
Texto(String),
Coordenadas { x: f64, y: f64 }, // struct-like
Cerrar,
}
// Uso
let msg = Mensaje::Texto(String::from("hola"));
match msg {
Mensaje::Texto(s) => println!("{}", s),
Mensaje::Coordenadas { x, y } => println!("{},{}", x, y),
Mensaje::Cerrar => println!("adios"),
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
enum Resultado { Exito(i32), Error(String) }
#[test]
fn test_enum() {
let r = Resultado::Exito(42);
match r {
Resultado::Exito(v) => assert_eq!(v, 42),
_ => panic!("esperaba éxito"),
}
}
#[test]
fn test_option() {
let x: Option<i32> = Some(10);
assert_eq!(x.unwrap(), 10);
assert_eq!(x.unwrap_or(0), 10);
assert_eq!(None::<i32>.unwrap_or(0), 0);
}
}
💡 Memoria y rendimiento
- El tamaño de un enum es el de su variante más grande + discriminant (tag)
Option<&T>tiene tamaño 0 overhead (optimización de puntero nulo)- El discriminant se almacena como un entero pequeño (1-4 bytes normalmente)
- Pattern matching en enum es tan rápido como un switch en C
✅ Buenas prácticas
- ✅ Usa
Option<T>en lugar de null pointers - ✅ Usa
Result<T, E>para operaciones que pueden fallar - ⚠️ No uses
unwrap()en producción — prefierematcho? - ❌ No crees enums con variantes que deberían ser structs separados
🏗️ Metodología
- Enums pequeños: modelan estados finitos (Activo/Inactivo/Pendiente)
- Enums grandes: pueden representar ASTs, comandos, protocolos
- Combina enums con traits para comportamientos polimórficos sin herencia
🔗 Para saber más
String vs &str
¿Qué es?
Rust tiene dos tipos principales de cadenas: String (propietaria, heap, mutable) y &str (prestada, vista de una cadena). Es una distinción fundamental para entender la gestión de memoria en Rust.
Sintaxis básica
let s: &str = "Hola"; // string literal (hardcodeado en binario)
let owned: String = "Hola".to_string();
let from_str: String = String::from("Hola");
// Conversiones
let vista: &str = &owned; // String → &str
let otra_owned: String = vista.to_string(); // &str → String
let slice: &str = &owned[0..4]; // slicing (cuidado con bytes vs chars)
// Iteración segura
for c in "ñññ".chars() {
println!("{c}");
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_string_types() {
let s: &str = "hello";
let owned: String = s.to_string();
assert_eq!(owned, "hello");
assert_eq!(&owned[..], s);
}
#[test]
fn test_string_concatenacion() {
let mut s = String::from("hola ");
s.push_str("mundo");
s.push('!');
assert_eq!(s, "hola mundo!");
}
#[test]
fn test_chars_vs_bytes() {
assert_eq!("ñ".len(), 2); // bytes
assert_eq!("ñ".chars().count(), 1); // caracteres
}
}
💡 Memoria y rendimiento
&strapunta a una secuencia contigua de bytes UTF-8 (len, capacidad, datos)&strpuede estar en stack (vista), heap (vista de String), o data segment (literales)Stringtiene 3 palabras: puntero, longitud, capacidad (24 bytes en 64-bit)- Slicing con índices byte puede romper caracteres multi-byte — usa
chars()para safe iteration
✅ Buenas prácticas
- ✅ Usa
&strpara parámetros de función (más flexible: acepta String y &str) - ✅ Usa
Stringcuando necesites propietario y mutabilidad - ⚠️ No indices bytes directamente en UTF-8 — prefiere métodos de
char/str - ❌ No uses
Stringdonde baste&str— es un 50% más grande en memoria
🏗️ Metodología
- APIs públicas: parámetros
&str, retornoString - Procesamiento de texto:
Stringpara construcción,&strpara análisis - Constantes y literales: siempre
&str(vive en el binario)
🔗 Para saber más
Vec<T> vs &[T]
¿Qué es?
Vec<T> es un array dinámico propietario (heap, redimensionable). &[T] es una vista prestada de una secuencia contigua de elementos. La relación es análoga a String vs &str.
Sintaxis básica
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
let v2 = vec![1, 2, 3]; // macro
// Slice (vista prestada)
let slice: &[i32] = &v[0..2];
let slice_completo: &[i32] = &v;
// Iteración
for x in &v { println!("{x}"); }
for x in v.iter() { println!("{x}"); }
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_vec() {
let mut v = vec![1, 2, 3];
v.push(4);
assert_eq!(v, vec![1, 2, 3, 4]);
assert_eq!(v.pop(), Some(4));
}
#[test]
fn test_slice() {
let v = vec![1, 2, 3, 4, 5];
let s = &v[1..4];
assert_eq!(s, &[2, 3, 4]);
}
#[test]
fn test_vec_capacity() {
let mut v = Vec::with_capacity(10);
assert!(v.capacity() >= 10);
v.push(1);
assert_eq!(v.len(), 1);
}
}
💡 Memoria y rendimiento
Vec<T>tiene 3 palabras (puntero, len, capacity) — 24 bytes overhead- El crecimiento es exponencial (factor ~1.5-2) para amortiguar reallocaciones
&[T]es 2 palabras (puntero, len) — 16 bytes, no tiene capacidad- Acceso por índice es O(1) en ambos
✅ Buenas prácticas
- ✅ Usa
Vec::with_capacity(n)cuando sepas el tamaño aproximado - ✅ Prefiere
&[T]en parámetros de función sobre&Vec<T> - ⚠️ Redimensionar un
Vecmuy grande puede ser costoso - ❌ No retornes referencias a datos locales de la función
🏗️ Metodología
Vec<T>: colección dinámica principal, casi siempre la elección correcta&[T]: lectura de secuencias sin ownership- Arrays
[T; N]: cuando el tamaño se conoce en compilación
🔗 Para saber más
Mutabilidad: const vs let vs let mut
¿Qué es?
Rust distingue tres niveles de inmutabilidad: const (constante de compilación), let (variable inmutable), let mut (variable mutable). Es una elección deliberada: la inmutabilidad por defecto evita efectos secundarios no deseados.
Sintaxis básica
const MAX_PUNTOS: u32 = 100_000; // Compile-time, tipo obligatorio
let x = 5; // Inmutable: no se puede reasignar
let mut y = 5; // Mutable
y = 10; // ✓ permitido
// x = 10; // ❌ error: variable inmutable
// Shadowing: re-declarar con mismo nombre
let nombre = " Hola ";
let nombre = nombre.trim(); // nuevo binding
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
const CONST_VAL: i32 = 42;
#[test]
fn test_const() {
assert_eq!(CONST_VAL, 42);
}
#[test]
fn test_inmutabilidad() {
let x = 5;
// x = 10; // no compilaría
assert_eq!(x, 5);
}
#[test]
fn test_mutabilidad() {
let mut x = 5;
x += 1;
assert_eq!(x, 6);
}
#[test]
fn test_shadowing() {
let x = 5;
let x = x * 2; // shadow
assert_eq!(x, 10);
}
}
💡 Memoria y rendimiento
constse inlinea en el punto de uso: cero overhead en runtimeletvslet mutno tiene diferencia de rendimiento: la mutabilidad es una restricción del compilador- El shadowing permite reusar el nombre sin mutar la variable original
✅ Buenas prácticas
- ✅ Usa
constpara valores mágicos y configuración en compilación - ✅ Prefiere
let(inmutable) por defecto; solo usalet mutcuando sea necesario - ⚠️ No abuses del shadowing: puede confundir en scopes grandes
- ❌ No uses mutabilidad global (
static mut) sinunsafe
🏗️ Metodología
const: valores fijos conocidos en compilaciónlet: variables que no cambian tras la inicialización (preferido)let mut: estado local que evoluciona (contadores, acumuladores)
🔗 Para saber más
Control de flujo y modularidad
Condicionales: if/else (expresión)
¿Qué es?
En Rust, if/else no es un statement — es una expresión que retorna un valor. Esto permite asignar el resultado de un condicional directamente a una variable. Es una de las influencias funcionales del lenguaje.
Sintaxis básica
let x = 5;
let resultado = if x > 0 { "positivo" } else { "no positivo" };
// if/else if/else como expresión
let categoria = if x > 0 {
"positivo"
} else if x < 0 {
"negativo"
} else {
"cero"
};
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_if_expresion() {
let cond = true;
let val = if cond { 10 } else { 20 };
assert_eq!(val, 10);
}
#[test]
fn test_if_encadenado() {
let x = 0;
let res = if x > 0 { "pos" } else if x < 0 { "neg" } else { "cero" };
assert_eq!(res, "cero");
}
}
💡 Memoria y rendimiento
- Las expresiones if se compilan a saltos condicionales como en C
- No hay overhead: el compilador elige la rama en runtime con una instrucción
cmp+jmp - Ambas ramas deben ser del mismo tipo (el compilador lo exige)
✅ Buenas prácticas
- ✅ Usa if como expresión cuando el resultado se asigna directamente
- ✅ Prefiere
matchpara 3+ ramas sobre if/else if/else - ⚠️ No anides if dentro de if — refactoriza a match o guard clauses
- ❌ No uses if sin else cuando se asigna el resultado
🏗️ Metodología
- 2 ramas: if/else
- 3+ ramas: match
- Validación previa: guard clauses con
if cond { return }al inicio
🔗 Para saber más
Loops: loop, while, for
¿Qué es?
Rust ofrece tres tipos de bucles: loop (infinito con break), while (condicional), for (iteración sobre colecciones). loop es único de Rust porque puede retornar un valor con break.
Sintaxis básica
// loop: infinito hasta break, puede retornar valor
let contador = loop {
break 42;
};
// while: condicional
let mut n = 3;
while n > 0 {
n -= 1;
}
// for: iteración idiomática
let v = vec![10, 20, 30];
for elemento in &v {
println!("{elemento}");
}
// Rango
for i in 0..5 { println!("{i}"); } // 0..4
for i in 0..=5 { println!("{i}"); } // 0..5
// Enumerate
for (i, val) in v.iter().enumerate() {
println!("v[{i}] = {val}");
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_loop_retorna_valor() {
let r = loop { break 42; };
assert_eq!(r, 42);
}
#[test]
fn test_for_range() {
let mut suma = 0;
for i in 0..=5 { suma += i; }
assert_eq!(suma, 15); // 0+1+2+3+4+5
}
#[test]
fn test_for_iter() {
let v = vec![1, 2, 3];
let mut suma = 0;
for x in &v { suma += x; }
assert_eq!(suma, 6);
}
#[test]
fn test_break_label() {
let mut count = 0;
'outer: loop {
loop {
count += 1;
if count == 3 { break 'outer; }
}
}
assert_eq!(count, 3);
}
}
💡 Memoria y rendimiento
forsobre iteradores se compila a código tan eficiente como while manual (zero-cost)- Los iteradores son lazy: no crean colecciones intermedias
loopconbreakretornando valor no tiene overhead extra
✅ Buenas prácticas
- ✅ Usa
forpor defecto para iterar colecciones — es más idiomático - ✅ Usa
whilepara bucles con condición mutable - ✅ Usa
loopcuando necesites retornar un valor con break - ⚠️ No uses
whilecon índices cuandoforcon iterador es más claro - ❌ No abuses de
breakcon labels — suele indicar que el bucle hace demasiadas cosas
🏗️ Metodología
for i in iter: 90% de los bucleswhile cond: bucles con condición mutable no ligada a una colecciónloop: drivers, event loops, servidores
🔗 Para saber más
Match (exhaustivo obligatorio)
¿Qué es?
match es la herramienta de control de flujo más poderosa de Rust. Compara un valor contra una serie de patrones y debe cubrir todos los casos posibles (exhaustividad). El compilador rechazará cualquier match no exhaustivo.
Sintaxis básica
let numero = 2;
let texto = match numero {
1 => "uno",
2 => "dos",
3 => "tres",
_ => "otro", // comodín: cubre el resto
};
// Con enum
enum Color { Rojo, Verde, Azul }
let c = Color::Verde;
let nombre = match c {
Color::Rojo => "rojo",
Color::Verde => "verde",
Color::Azul => "azul",
// No necesitamos _ porque cubrimos todas las variantes
};
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_match_exhaustivo() {
let x = 3;
let res = match x {
1 => "uno",
2 => "dos",
_ => "muchos",
};
assert_eq!(res, "muchos");
}
#[test]
fn test_match_option() {
let val = Some(42);
let result = match val {
Some(v) => v * 2,
None => 0,
};
assert_eq!(result, 84);
}
}
💡 Memoria y rendimiento
matchse compila a tabla de saltos o comparaciones encadenadas, como un switch en C- El compilador optimiza el orden de las ramas para minimizar comparaciones
- En enums con discriminant pequeño, el salto es O(1) (tabla)
✅ Buenas prácticas
- ✅ Usa
matchpara 3+ ramas en lugar de if/else if - ✅ Usa
_como comodín para el caso por defecto - ⚠️ El orden importa: las ramas se evalúan de arriba abajo
- ❌ No uses
matchcuandoif letes más conciso (un solo patrón)
🏗️ Metodología
- Match exhaustivo: obligatorio para enums (el compilador exige cubrir todo)
- Match con wildcard: útil cuando solo importan algunos casos específicos
- Match con guards: añade condiciones adicionales a los patrones
🔗 Para saber más
if let / while let
¿Qué es?
if let y while let son azúcar sintáctico para match cuando solo te interesa un patrón. Son ideales para trabajar con Option y Result sin el boilerplate del match completo.
Sintaxis básica
let valor = Some(42);
// En lugar de:
match valor {
Some(v) => println!("{v}"),
_ => {},
}
// if let simplifica:
if let Some(v) = valor {
println!("{v}");
}
// while let: extraer mientras el patrón coincida
let mut pila = vec![1, 2, 3];
while let Some(tope) = pila.pop() {
println!("{tope}");
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_if_let_option() {
let val = Some(42);
let mut result = 0;
if let Some(v) = val {
result = v;
}
assert_eq!(result, 42);
}
#[test]
fn test_while_let() {
let mut v = vec![1, 2, 3];
let mut sum = 0;
while let Some(x) = v.pop() {
sum += x;
}
assert_eq!(sum, 6);
}
}
💡 Memoria y rendimiento
if letse compila exactamente igual que unmatchcon un solo brazo- No hay penalización por usar el azúcar sintáctico
while lettiene el mismo rendimiento queloop { match } { break }
✅ Buenas prácticas
- ✅ Usa
if letcuando solo te interese un patrón - ✅ Usa
while letpara bucles que extraen de un Option/Iterator - ⚠️ Si necesitas también el caso else, usa
if let ... else - ❌ No uses
if letcuando necesites cubrir múltiples patrones — usamatch
🏗️ Metodología
if let Some(x): la forma idiomática de trabajar con Optionif let Ok(x): la forma idiomática de trabajar con Result (cuando no importa el error)while let Some(x): extraer elementos de un iterador sin for
🔗 Para saber más
Módulos y visibilidad
¿Qué es?
El sistema de módulos de Rust organiza el código en namespaces y controla la visibilidad con pub. Cada archivo es un módulo, y los directorios pueden ser módulos con mod.rs o nombre.rs.
Sintaxis básica
// src/main.rs
mod utils; // busca src/utils.rs
fn main() {
utils::saludar();
}
// src/utils.rs
pub fn saludar() {
println!("Hola!");
}
// Visibilidad
pub fn publica() {} // accesible fuera del módulo
fn privada() {} // solo dentro del módulo
pub(crate) fn interna() {} // dentro del crate
pub(super) fn padre() {} // solo en el módulo padre
🧪 Cómo probarlo
// src/lib.rs
pub mod math {
pub fn suma(a: i32, b: i32) -> i32 { a + b }
fn privada() {} // no accesible fuera
}
#[cfg(test)]
mod tests {
use super::math;
#[test]
fn test_modulo() {
assert_eq!(math::suma(2, 2), 4);
}
}
💡 Memoria y rendimiento
- Los módulos son puramente en tiempo de compilación: cero coste en runtime
- No afectan al layout de memoria ni al rendimiento
- El compilador inlinea funciones a través de fronteras de módulos si es beneficioso
✅ Buenas prácticas
- ✅ Usa
pub(crate)para APIs internas del crate - ✅ Organiza módulos por funcionalidad, no por tipo (ej:
mod databasemejor quemod structs) - ⚠️ No expongas campos privados de structs — usa métodos getter
- ❌ No crees módulos con una sola función — agrupa por dominio
🏗️ Metodología
- Proyecto pequeño (
< 5 archivos): módulos planos en src/ - Proyecto mediano: módulos anidados por funcionalidad
- Proyecto grande: subcrates o workspaces
🔗 Para saber más
Cargo.toml y workspaces
¿Qué es?
Cargo.toml es el manifiesto del proyecto. Los workspaces permiten gestionar múltiples crates relacionados en un solo repositorio, compartiendo dependencias y lockfile.
Sintaxis básica
[package]
name = "mi-proyecto"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
Workspace:
# Cargo.toml (raíz)
[workspace]
members = ["crates/*"]
resolver = "2"
🧪 Cómo probarlo
cargo check # Verifica todo el workspace
cargo test -p nombre-del-crate # Testea un crate específico
cargo build --workspace # Compila todo
💡 Memoria y rendimiento
- Cargo compila en paralelo dentro de un workspace
- Las dependencias comunes se comparten entre crates
target/se comparte en el workspace (un solo directorio de build)
✅ Buenas prácticas
- ✅ Usa workspaces para separar librería, binarios y tests
- ✅ Usa
[profile.release]conlto = trueycodegen-units = 1para optimizar - ⚠️ No pongas todo en un solo crate — separa por responsabilidad
- ❌ No abuses de workspaces para proyectos pequeños (añade complejidad)
🏗️ Metodología
- Proyecto pequeño: un solo crate
- Librería + binario: workspace con
crates/separados - Monorepo: workspace con múltiples paquetes inter-dependientes
🔗 Para saber más
Sistema de archivos (File System I/O)
std::fs (lectura/escritura básica)
¿Qué es?
std::fs es el módulo estándar para operaciones con el sistema de archivos: leer, escribir, crear, borrar archivos y directorios. Toda la E/S en Rust es bloqueante por defecto y devuelve Result para manejar errores.
Sintaxis básica
use std::fs;
use std::io;
fn main() -> io::Result<()> {
// Leer archivo completo
let contenido = fs::read_to_string("datos.txt")?;
println!("{}", contenido);
// Escribir archivo (sobrescribe)
fs::write("salida.txt", "Hola mundo")?;
// Leer binario
let bytes = fs::read("imagen.png")?;
// Crear directorio
fs::create_dir("nueva_carpeta")?;
// create_dir_all crea toda la jerarquía
fs::create_dir_all("a/b/c")?;
// Listar directorio
for entrada in fs::read_dir(".")? {
let entrada = entrada?;
println!("{}", entrada.path().display());
}
Ok(())
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use std::fs;
use std::io;
#[test]
fn test_write_and_read() -> io::Result<()> {
let path = "test_archivo.txt";
fs::write(path, "test contenido")?;
let contenido = fs::read_to_string(path)?;
assert_eq!(contenido, "test contenido");
fs::remove_file(path)?;
Ok(())
}
#[test]
fn test_create_and_remove_dir() -> io::Result<()> {
fs::create_dir("test_dir")?;
assert!(fs::metadata("test_dir")?.is_dir());
fs::remove_dir("test_dir")?;
Ok(())
}
}
💡 Memoria y rendimiento
read_to_stringcarga todo el archivo en memoria — no apto para archivos grandesreadcarga comoVec<u8>: el archivo completo en heap- Para archivos grandes, usa
BufReader/BufWriter(lectura/escritura por fragmentos) - Las operaciones de FS son lentas (orden de ms) — considéralas I/O bound
✅ Buenas prácticas
- ✅ Usa
read_to_stringsolo para archivos pequeños (config, logs pequeños) - ✅ Usa
writepara sobrescritura completa de archivos pequeños - ⚠️ Siempre maneja
io::Result— no asumas que un archivo existe - ❌ No uses
read_to_stringpara archivos de más de unos pocos MB
🏗️ Metodología
- Archivos pequeños (< 1 MB):
read_to_string/write - Archivos grandes:
BufReader+BufWriter - Directorios:
read_dircon filter/map para procesar
🔗 Para saber más
BufReader / BufWriter
¿Qué es?
BufReader y BufWriter añaden un buffer interno para reducir las llamadas al sistema (syscalls). En lugar de leer byte a byte, leen en bloques (por defecto 8KB). Esencial para procesar archivos grandes de forma eficiente.
Sintaxis básica
use std::fs::File;
use std::io::{BufRead, BufReader, Write, BufWriter};
// Leer línea por línea (eficiente)
let archivo = File::open("datos.txt")?;
let reader = BufReader::new(archivo);
for linea in reader.lines() {
let linea = linea?;
println!("{}", linea);
}
// Escribir con buffer
let archivo = File::create("salida.txt")?;
let mut writer = BufWriter::new(archivo);
writeln!(writer, "Hola mundo")?;
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use std::fs::File;
use std::io::{BufRead, BufReader, Write, BufWriter};
#[test]
fn test_bufreader() -> std::io::Result<()> {
let file = File::open("Cargo.toml")?;
let reader = BufReader::new(file);
let mut lineas = 0;
for _ in reader.lines() { lineas += 1; }
assert!(lineas > 0);
Ok(())
}
#[test]
fn test_bufwriter() -> std::io::Result<()> {
let file = File::create("test_buf.txt")?;
let mut writer = BufWriter::new(file);
writeln!(writer, "línea 1")?;
writeln!(writer, "línea 2")?;
writer.flush()?;
Ok(())
}
}
💡 Memoria y rendimiento
- BufReader reduce las syscalls de N a N/buffer_size (8KB por defecto)
- Sin buffer: cada
read()es una syscall (~1µs de overhead) - Con buffer: se leen 8KB de una vez, se sirven desde memoria
- BufWriter hace lo mismo en escritura: acumula hasta 8KB antes de escribir
✅ Buenas prácticas
- ✅ Usa BufReader para archivos de más de unos pocos KB
- ✅ Usa
read_untilolinespara procesamiento línea a línea - ⚠️
flush()o el Drop de BufWriter escribe el buffer restante - ❌ No envuelvas un archivo pequeño en BufReader si solo haces una lectura
🏗️ Metodología
- BufReader: archivos de texto grandes (logs, CSVs, archivos de datos)
- BufWriter: archivos de salida grandes (resultados, exportaciones)
- Sin buffer: lecturas de una sola vez con
read_to_string(archivos pequeños)
🔗 Para saber más
std::path::Path y PathBuf
¿Qué es?
Path es una slice de un path de archivo (como &str para strings). PathBuf es la versión propietaria (como String). Proporcionan métodos multiplataforma para manipular rutas.
Sintaxis básica
use std::path::{Path, PathBuf};
// PathBuf (propietario, mutable)
let mut ruta = PathBuf::from("/usr/local/bin");
ruta.push("mi-app");
assert_eq!(ruta, Path::new("/usr/local/bin/mi-app"));
// Path (vista prestada)
let p = Path::new("/home/user/documentos/archivo.txt");
println!("padre: {:?}", p.parent());
println!("nombre: {:?}", p.file_name());
println!("extensión: {:?}", p.extension());
println!("sin extensión: {:?}", p.file_stem());
// Métodos útiles
assert!(p.is_absolute());
assert!(!p.is_relative());
assert_eq!(p.to_str(), Some("/home/user/documentos/archivo.txt"));
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
#[test]
fn test_path_manipulation() {
let mut pb = PathBuf::from("/a");
pb.push("b");
pb.push("c.txt");
assert_eq!(pb, Path::new("/a/b/c.txt"));
}
#[test]
fn test_path_components() {
let p = Path::new("/usr/local/bin");
assert_eq!(p.parent(), Some(Path::new("/usr/local")));
assert_eq!(p.file_name(), Some(std::ffi::OsStr::new("bin")));
}
#[test]
fn test_path_extension() {
let p = Path::new("datos.csv");
assert_eq!(p.extension(), Some(std::ffi::OsStr::new("csv")));
assert_eq!(p.file_stem(), Some(std::ffi::OsStr::new("datos")));
}
}
💡 Memoria y rendimiento
Pathes una slice (2 palabras: puntero + len)PathBufes 3 palabras (puntero, len, capacity) como String- Las operaciones de path no acceden al disco — solo manipulan strings
- Usa
PathBuf::with_capacitysi sabes la longitud máxima
✅ Buenas prácticas
- ✅ Usa
Pathen parámetros de función (más flexible que&strpara rutas) - ✅ Usa
PathBufpara construir y almacenar rutas - ⚠️ Usa
to_str()en lugar deto_string_lossy()solo si estás seguro de que es UTF-8 válido - ❌ No concatenes rutas con formato de strings — usa
pushojoin
🏗️ Metodología
Path: referencia a una ruta (parámetros de funciones)PathBuf: almacenar y construir rutasjoinvspush:joincrea un PathBuf nuevo,pushmodifica in-place
🔗 Para saber más
Procesar CSV y escribir JSON
¿Qué es?
El ecosistema Rust tiene crates especializados para formatos de datos. csv para CSV y serde_json para JSON son los estándares de la industria. Combinados con serde permiten serialización/deserialización automática.
Sintaxis básica
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::BufReader;
#[derive(Debug, Serialize, Deserialize)]
struct Usuario {
nombre: String,
edad: u8,
email: String,
}
// Leer CSV
let archivo = File::open("usuarios.csv")?;
let reader = BufReader::new(archivo);
let mut csv_reader = csv::Reader::from_reader(reader);
for result in csv_reader.deserialize() {
let usuario: Usuario = result?;
println!("{:?}", usuario);
}
// Escribir JSON
let usuarios = vec![
Usuario { nombre: "Alice".into(), edad: 30, email: "alice@ex.com".into() },
];
let json = serde_json::to_string_pretty(&usuarios)?;
fs::write("usuarios.json", json)?;
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Punto { x: i32, y: i32 }
#[test]
fn test_json_serialize() {
let p = Punto { x: 1, y: 2 };
let json = serde_json::to_string(&p).unwrap();
assert_eq!(json, r#"{"x":1,"y":2}"#);
}
#[test]
fn test_json_deserialize() {
let json = r#"{"x":3,"y":4}"#;
let p: Punto = serde_json::from_str(json).unwrap();
assert_eq!(p, Punto { x: 3, y: 4 });
}
}
💡 Memoria y rendimiento
- serde_json serializa/deserializa en memoria: O(n) en el tamaño de los datos
- csv::Reader es streaming: no carga todo el archivo en memoria
- serde usa derive macros que se ejecutan en compilación (zero-cost)
- Para archivos enormes, usa csv + serde en modo streaming
✅ Buenas prácticas
- ✅ Añade serde con
cargo add serde --features derive - ✅ Usa
#[serde(rename_all = "snake_case")]para convenciones - ⚠️ No serialices tipos con referencias internas — serde necesita owned data
- ❌ No uses
serde_json::from_stren archivos enormes — prefierefrom_reader
🏗️ Metodología
- CSV: datos tabulares grandes, logs, exportaciones de bases de datos
- JSON: APIs, configuración, intercambio de datos entre servicios
- Para rendimiento extremo: considera
bincode,msgpack, oprotobuf
🔗 Para saber más
Algoritmos y estructuras de datos
Ordenación
¿Qué es?
Rust proporciona ordenación estable (sort) e inestable (sort_unstable) en slices. La ordenación por defecto usa el orden natural del tipo (a través del trait Ord), y se puede personalizar con sort_by.
Sintaxis básica
let mut nums = vec![3, 1, 4, 1, 5];
nums.sort();
assert_eq!(nums, vec![1, 1, 3, 4, 5]);
let mut floats = vec![3.2, 1.5, 4.1];
floats.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
// Ordenación personalizada
let mut items = vec![(3, "c"), (1, "a"), (2, "b")];
items.sort_by(|a, b| a.0.cmp(&b.0));
// sort por campo
items.sort_by_key(|k| k.0);
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_sort() {
let mut v = vec![3, 1, 4, 1, 5];
v.sort();
assert_eq!(v, vec![1, 1, 3, 4, 5]);
}
#[test]
fn test_sort_reverse() {
let mut v = vec![1, 2, 3];
v.sort_by(|a, b| b.cmp(a));
assert_eq!(v, vec![3, 2, 1]);
}
#[test]
fn test_sort_by_key() {
let mut v = [(3, "c"), (1, "a"), (2, "b")];
v.sort_by_key(|k| k.0);
assert_eq!(v, [(1, "a"), (2, "b"), (3, "c")]);
}
}
💡 Memoria y rendimiento
sort_unstablees más rápido pero no preserva el orden de elementos igualessort(estable) usa merge sort optimizado: O(n log n) peor caso, O(n) mejor casosort_unstableusa pattern-defeating quicksort: O(n log n) promediosort_unstableno asigna memoria extra;sortpuede asignar un buffer temporal
✅ Buenas prácticas
- ✅ Usa
sort_unstablepor defecto — es más rápido y no asigna memoria - ✅ Usa
sortsolo cuando necesites estabilidad (orden relativo preservado) - ⚠️
f32/f64no implementanOrd(NaN no es ordenable) — usasort_byconpartial_cmp - ❌ No implementes tu propia ordenación a menos que tengas datos muy concretos
🏗️ Metodología
- sort: estabilidad necesaria (ej: ordenar por fecha, luego por nombre)
- sort_unstable: rendimiento crítico, estabilidad no necesaria
- sort_by_key: ordenar por un campo calculado sin crear struct temporal
🔗 Para saber más
Búsqueda
¿Qué es?
Rust ofrece búsqueda binaria en slices ordenados con binary_search, y búsqueda lineal con los iteradores find y position. La búsqueda binaria es O(log n) pero requiere datos ordenados.
🔗 Rust std::slice::binary_search
Sintaxis básica
let nums = vec![1, 3, 5, 7, 9];
// Búsqueda binaria (O(log n))
match nums.binary_search(&5) {
Ok(i) => println!("encontrado en índice {i}"),
Err(i) => println!("debería estar en índice {i}"),
}
// Búsqueda lineal con iteradores
let encontrado = nums.iter().find(|&&x| x == 5);
let posicion = nums.iter().position(|&x| x == 5);
// Búsqueda en vectores no ordenados
let items = vec!["a", "b", "c"];
assert!(items.contains(&"b"));
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_binary_search() {
let v = vec![1, 3, 5, 7, 9];
assert_eq!(v.binary_search(&5), Ok(2));
assert_eq!(v.binary_search(&4), Err(2));
}
#[test]
fn test_find() {
let v = vec![10, 20, 30, 40];
let result: Option<&i32> = v.iter().find(|&&x| x > 25);
assert_eq!(result, Some(&30));
}
#[test]
fn test_contains() {
let v = vec!["manzana", "pera", "uva"];
assert!(v.contains(&"pera"));
assert!(!v.contains(&"platano"));
}
}
💡 Memoria y rendimiento
binary_search: O(log n), requiere datos ordenados, no asigna memoriafind/position: O(n), early exit al encontrar el primer matchcontains: O(n) en slices/vec (lineal), O(1) en HashSet- Para búsquedas frecuentes, considera convertir a
HashSetoBTreeSet
✅ Buenas prácticas
- ✅ Usa
binary_searchen datos ordenados (más rápido que find) - ✅ Usa
findpara búsquedas con condición compleja - ⚠️ binary_search requiere el tipo
Ord— no funciona con floats directamente - ❌ No conviertas a HashSet para una sola búsqueda — el overhead de hash es mayor
🏗️ Metodología
- Una búsqueda:
find(lineal) - Muchas búsquedas en datos estáticos: ordenar +
binary_search - Muchas búsquedas en datos dinámicos:
HashSet/BTreeSet
🔗 Para saber más
Iterator API completa
¿Qué es?
Los iteradores en Rust son lazy, zero-cost y componibles. El compilador optimiza cadenas completas de iteradores a código máquina tan eficiente como un loop manual. La API de Iterator proporciona docenas de métodos adaptadores.
Sintaxis básica
let nums = vec![1, 2, 3, 4, 5, 6];
// Chain de iteradores (zero-cost)
let resultado: Vec<i32> = nums.iter()
.filter(|x| *x % 2 == 0) // pares: 2, 4, 6
.map(|x| x * 2) // doble: 4, 8, 12
.take(2) // solo 2: 4, 8
.collect();
// fold (reduce)
let suma = nums.iter().fold(0, |acc, x| acc + x);
// any / all
let hay_pares = nums.iter().any(|x| x % 2 == 0);
let todos_pares = nums.iter().all(|x| x % 2 == 0);
// flat_map
let nested = vec![vec![1, 2], vec![3, 4]];
let flat: Vec<i32> = nested.into_iter().flatten().collect();
// chain: concatenar dos iteradores
let a = vec![1, 2];
let b = vec![3, 4];
let ab: Vec<i32> = a.into_iter().chain(b).collect();
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_iterator_chain() {
let v: Vec<i32> = (1..=5)
.filter(|x| x % 2 == 0)
.map(|x| x * 10)
.collect();
assert_eq!(v, vec![20, 40]);
}
#[test]
fn test_fold() {
let sum: i32 = (1..=100).fold(0, |acc, x| acc + x);
assert_eq!(sum, 5050);
}
#[test]
fn test_any_all() {
let v = vec![1, 2, 3, 4, 5];
assert!(v.iter().any(|x| x > 3));
assert!(!v.iter().all(|x| x > 3));
}
#[test]
fn test_flat_map() {
let v = vec![vec![1, 2], vec![3, 4]];
let flat: Vec<i32> = v.into_iter().flatten().collect();
assert_eq!(flat, vec![1, 2, 3, 4]);
}
}
💡 Memoria y rendimiento
- Los adaptadores de iterador son cero coste: el compilador los fusiona en un solo loop
collectes el único punto donde se asigna memoria- Los iteradores son lazy:
map/filterno ejecutan nada hasta que se consume - Usa
size_hintpara quecollectpre-asigne capacidad exacta
✅ Buenas prácticas
- ✅ Prefiere chains de iteradores sobre loops manuales con lógica compleja
- ✅ Usa
collectcon tipos concretos (Vec,HashMap,HashSet) - ⚠️ No abuses de
collectintermedios — rompe la lazy evaluation - ❌ No conviertas a Vec solo para iterar — usa el iterador directamente
🏗️ Metodología
map/filter: transformaciones de colecciones (90% de los casos)fold/reduce: agregaciones y acumulacionesflat_map/flatten: desanidar estructuras jerárquicaschain: concatenar varias fuentes de datos
🔗 Para saber más
Big O de colecciones estándar
| Operación | Vec | VecDeque | LinkedList | HashMap | BTreeMap | HashSet | BTreeSet |
|---|---|---|---|---|---|---|---|
| Acceso | O(1) | O(1) | O(n) | O(1)* | O(log n) | — | — |
| Inserción final | O(1)** | O(1) | O(1) | — | — | — | — |
| Inserción inicio | O(n) | O(1) | O(1) | — | — | — | — |
| Búsqueda | O(n) | O(n) | O(n) | O(1)* | O(log n) | O(1)* | O(log n) |
| Eliminación | O(n) | O(n) | O(1) | O(1)* | O(log n) | O(1)* | O(log n) |
| Ordenación | O(n log n) | O(n log n) | — | — | — | — | — |
*Amortizado, asumiendo buen hash function **Amortizado O(1); O(n) cuando hay que redimensionar
✅ Buenas prácticas
- ✅ Vec: la colección por defecto para secuencias
- ✅ HashMap: mapeo clave-valor con O(1) promedio
- ✅ BTreeMap: mapeo ordenado, O(log n) consistente
- ⚠️ LinkedList casi nunca es necesario en Rust — Vec o VecDeque son mejores
- ❌ No uses HashMap con claves que no implementan Eq + Hash
🔗 Para saber más
Conceptos clave del lenguaje
Ownership, Borrowing y Lifetimes
¿Qué es?
Ownership es el sistema de gestión de memoria de Rust: cada valor tiene un único propietario, y se libera automáticamente cuando el propietario sale de scope. Borrowing permite prestar una referencia sin transferir la propiedad. Lifetimes son anotaciones que verifican que las referencias vivan lo suficiente.
Sintaxis básica
// Ownership: cada valor tiene un único dueño
let s1 = String::from("hola");
let s2 = s1; // s1 se mueve a s2
// println!("{}", s1); // ❌ error: s1 ya no es válido
// Borrowing: prestar sin mover
let s3 = String::from("mundo");
let len = calcular_largo(&s3); // &s3 presta una referencia
println!("{}", s3); // s3 sigue siendo válido
fn calcular_largo(s: &str) -> usize { s.len() }
// Mutable borrowing: una sola referencia mutable a la vez
let mut s = String::from("hola");
let r1 = &mut s;
// let r2 = &mut s; // ❌ error: dos refs mut simultáneas no permitidas
// Lifetimes: las referencias deben vivir lo suficiente
fn el_mas_largo<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_ownership_move() {
let s1 = String::from("hola");
let s2 = s1;
// s1 ya no es válido aquí
assert_eq!(s2, "hola");
}
#[test]
fn test_borrowing() {
let s = String::from("mundo");
let r = &s;
assert_eq!(*r, "mundo");
assert_eq!(s, "mundo"); // s sigue vivo
}
#[test]
fn test_mutable_borrow() {
let mut s = String::from("hola");
let r = &mut s;
r.push_str(" mundo");
assert_eq!(*r, "hola mundo");
}
#[test]
fn test_lifetime_elision() {
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
assert_eq!(first_word("hola mundo"), "hola");
}
}
💡 Memoria y rendimiento
- Ownership no tiene overhead en runtime: el compilador inserta
free/dropen compile-time - Las referencias (
&T) son punteros de tamaño palabra (8 bytes en 64-bit) - El borrow checker se ejecuta en compilación: cero impacto en rendimiento
- Los lifetimes son puramente en compilación: no existen en runtime
✅ Buenas prácticas
- ✅ Prefiere referencias (
&T) sobre ownership cuando no necesites propietario - ✅ Usa
&mut Tsolo cuando necesites modificar - ⚠️ No luches contra el borrow checker — suele indicar un diseño mejorable
- ❌ No abuses de
clone()para evitar problemas de ownership — aprende a prestar
🏗️ Metodología
- Funciones que solo leen datos:
&T(referencia compartida) - Funciones que modifican:
&mut T(referencia mutable) - Funciones que consumen:
T(ownership) — el caller pierde acceso - Funciones que crean datos: retorna
T
🔗 Para saber más
Option y Result
¿Qué es?
Option<T> representa un valor que puede ser Some(T) o None (alternativa a null pointers). Result<T, E> representa una operación que puede ser Ok(T) o Err(E) (alternativa a excepciones). Ambos son enums y el compilador exige que los manejes.
🔗 Rust std::option, Rust std::result
Sintaxis básica
// Option
fn dividir(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
let resultado = dividir(10.0, 2.0);
match resultado {
Some(v) => println!("{v}"),
None => println!("error"),
}
// Combinadores
let doblado = dividir(10.0, 2.0).map(|x| x * 2.0);
let con_default = dividir(10.0, 0.0).unwrap_or(0.0);
// Result
fn leer_archivo(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
// ? operator (propagación de errores)
fn procesar() -> Result<(), std::io::Error> {
let contenido = leer_archivo("datos.txt")?; // Si error, retorna
println!("{}", contenido);
Ok(())
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
fn mitad(x: i32) -> Option<i32> {
if x % 2 == 0 { Some(x / 2) } else { None }
}
#[test]
fn test_option_some() {
assert_eq!(mitad(4), Some(2));
}
#[test]
fn test_option_none() {
assert_eq!(mitad(3), None);
}
#[test]
fn test_option_combinators() {
let r = mitad(10).map(|x| x * 3);
assert_eq!(r, Some(15));
let r = mitad(3).unwrap_or(-1);
assert_eq!(r, -1);
}
#[test]
fn test_result() {
let ok: Result<i32, &str> = Ok(42);
assert_eq!(ok.unwrap(), 42);
let err: Result<i32, &str> = Err("fallo");
assert_eq!(err.unwrap_or(0), 0);
}
}
💡 Memoria y rendimiento
Option<&T>tiene tamaño 0 overhead (optimización de puntero nulo: niche)Option<Box<T>>también es del tamaño de un puntero (niche optimization)Result<T, E>ocupa tamaño máximo entre T y E + discriminant?operator se compila a early return condicional — tan eficiente como un goto
✅ Buenas prácticas
- ✅ Usa
Optionpara valores opcionales (mejor que null) - ✅ Usa
Resultpara operaciones que pueden fallar - ✅ Usa
?para propagar errores — evitaunwrapen producción - ⚠️ No uses
unwrap()oexpect()sin una razón muy justificada - ❌ No uses
NonecomoSome(error_sin_info)— usa Result con errores concretos
🏗️ Metodología
Option-> operaciones que pueden no tener resultado (ej: buscar en colección)Result-> operaciones que pueden fallar con causa (ej: E/S, parseo)?-> propagación de errores en funciones que retornan Result- Combinadores (map, and_then, or_else) -> transformaciones sin match explícito
🔗 Para saber más
Error handling: thiserror y anyhow
¿Qué es?
thiserror permite definir errores personalizados con derive macros. anyhow proporciona un tipo Error dinámico para errores ad-hoc. thiserror es para librerías (errores concretos), anyhow para aplicaciones (errores genéricos).
🔗 thiserror crate, anyhow crate
Sintaxis básica
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MiError {
#[error("archivo no encontrado: {0}")]
NoEncontrado(String),
#[error("error de red: {0}")]
Red(#[from] std::io::Error),
#[error("formato inválido en línea {linea}")]
Formato { linea: usize },
}
// Uso con anyhow (para aplicaciones)
use anyhow::{Result, Context};
fn procesar() -> Result<()> {
let data = std::fs::read_to_string("datos.txt")
.context("no se pudo leer el archivo de datos")?;
// ...
Ok(())
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
enum CalcError {
#[error("división por cero")]
DivisionPorCero,
#[error("desbordamiento")]
Desbordamiento,
}
fn dividir(a: i32, b: i32) -> Result<i32, CalcError> {
if b == 0 { Err(CalcError::DivisionPorCero) }
else { Ok(a / b) }
}
#[test]
fn test_error_personalizado() {
assert_eq!(dividir(10, 0), Err(CalcError::DivisionPorCero));
assert_eq!(dividir(10, 2), Ok(5));
}
#[test]
fn test_error_mensaje() {
let err = CalcError::DivisionPorCero;
assert_eq!(err.to_string(), "división por cero");
}
}
💡 Memoria y rendimiento
thiserrorgenera tipos concretos en compilación: cero overhead en runtimeanyhow::Errores un trait object: asigna en heap y tiene indirecciónanyhowes ideal para prototipos y aplicaciones;thiserrorpara librerías- La macro
#[from]genera implFromautomático (sin overhead extra)
✅ Buenas prácticas
- ✅ Usa
thiserroren librerías: errores concretos, documentados, exhaustivos - ✅ Usa
anyhowen binarios: errores ad-hoc con contexto - ⚠️ No mezcles
thiserroryanyhowen el mismo módulo sin necesidad - ❌ No definas errores genéricos como
String— usa enums con variantes específicas
🏗️ Metodología
- Librerías:
thiserrordevuelveResult<T, MiError>específico - Aplicaciones:
anyhowdevuelveanyhow::Result<T>genérico - La aplicación puede convertir errores de librerías con
Context
🔗 Para saber más
Traits, derives, bounds, impl Trait, dyn Trait
¿Qué es?
Los traits son la herramienta de abstracción de Rust (equivalente a interfaces en otros lenguajes). Definen comportamiento compartido. derive genera implementaciones automáticas. impl Trait y dyn Trait ofrecen dos formas de polimorfismo.
Sintaxis básica
// Definir un trait
trait Area {
fn area(&self) -> f64;
}
// Implementar
struct Circulo { radio: f64 }
impl Area for Circulo {
fn area(&self) -> f64 { std::f64::consts::PI * self.radio * self.radio }
}
// Derive automático
#[derive(Debug, Clone, PartialEq)]
struct Punto { x: i32, y: i32 }
// impl Trait (monomorfización)
fn imprimir_area(forma: &impl Area) {
println!("{}", forma.area());
}
// Trait bounds con genéricos
fn area_doble<T: Area>(forma: &T) -> f64 {
forma.area() * 2.0
}
// dyn Trait (trait objects, dispatch dinámico)
fn area_dinamica(forma: &dyn Area) -> f64 {
forma.area()
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
trait Sumable { fn sumar(&self, other: &Self) -> Self; }
impl Sumable for i32 {
fn sumar(&self, other: &i32) -> i32 { self + other }
}
fn sumar_gen<T: Sumable>(a: &T, b: &T) -> T {
a.sumar(b)
}
#[test]
fn test_trait() {
assert_eq!(sumar_gen(&5, &3), 8);
}
#[test]
fn test_derive_debug() {
#[derive(Debug)]
struct X { a: i32 }
let x = X { a: 42 };
assert_eq!(format!("{:?}", x), "X { a: 42 }");
}
}
💡 Memoria y rendimiento
impl Traity genéricos con bounds: monomorfización — código duplicado por tipo, máximo rendimientodyn Trait: vtable — indirección, un sola implementación, menor rendimiento- Los derives se expanden en compile-time: no afectan a runtime
- Los trait objects tienen tamaño de 2 palabras (data ptr + vtable ptr)
✅ Buenas prácticas
- ✅ Prefiere genéricos con
impl Traito bounds para máximo rendimiento - ✅ Usa
dyn Traitcuando necesites heterogeneidad (colecciones de diferentes tipos) - ✅ Deriva
Debug,Clone,PartialEqsiempre que tenga sentido - ⚠️ No abuses de
dyn Traiten hot paths — la indirección tiene coste - ❌ No implementes traits externos para tipos externos (orphan rule)
🏗️ Metodología
- Genéricos + bounds: APIs genéricas, librerías, hot paths
impl Trait: parámetros de función simplesdyn Trait: colecciones heterogéneas, plugins, inyección de dependencias
🔗 Para saber más
Smart Pointers: Box, Rc, Arc, Cell, RefCell, Mutex
¿Qué es?
Los smart pointers envuelven valores con comportamiento adicional. Box<T> asigna en heap, Rc<T> permite ownership compartido (single-thread), Arc<T> es thread-safe, Cell/RefCell proporcionan mutabilidad interior.
Sintaxis básica
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::cell::RefCell;
// Box: heap allocation
let b = Box::new(42);
let lista = Box::new(Lista::Nodo(1, Box::new(Lista::Nodo(2, Box::new(Lista::Fin)))));
// Rc: referencia contada (single-thread)
let a = Rc::new(42);
let b = Rc::clone(&a); // incrementa contador
println!("{}", Rc::strong_count(&a)); // 2
// Arc: referencia contada thread-safe
let a = Arc::new(42);
let b = Arc::clone(&a);
// RefCell: mutabilidad interior en tiempo de ejecución
let celda = RefCell::new(42);
*celda.borrow_mut() = 100;
println!("{}", celda.borrow());
// Mutex: acceso exclusivo entre threads
let m = Mutex::new(42);
{
let mut dato = m.lock().unwrap();
*dato = 100;
} // drop libera el lock
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use std::rc::Rc;
use std::sync::{Arc, Mutex};
#[test]
fn test_box() {
let b = Box::new(42);
assert_eq!(*b, 42);
}
#[test]
fn test_rc() {
let r = Rc::new(5);
assert_eq!(Rc::strong_count(&r), 1);
let _r2 = Rc::clone(&r);
assert_eq!(Rc::strong_count(&r), 2);
}
#[test]
fn test_mutex() {
let m = Mutex::new(10);
{
let mut v = m.lock().unwrap();
*v += 5;
}
assert_eq!(*m.lock().unwrap(), 15);
}
}
💡 Memoria y rendimiento
Box<T>: 1 palabra en stack (puntero), datos en heap. Sin overhead de contadorRc<T>: 2 palabras (puntero + contador). El contador esusize— no atómicoArc<T>: 2 palabras (puntero + contador atómico). Más caro en incrementos/decrementosRefCell<T>: verifica borrows en runtime (no en compilación) — paniquea si violas reglasMutex<T>: bloquea el hilo si está en uso. Overhead de syscall en contención
✅ Buenas prácticas
- ✅ Usa
Boxpara tipos de tamaño dinámico (enums recursivos, trait objects) - ✅ Usa
Rc/Arcpara ownership compartido (solo cuando no puedas tener un único owner) - ⚠️
RefCellrompe las garantías de borrow checking en compile-time — úsalo con cuidado - ❌ No uses
Rccuando necesites threads — usaArc - ❌ No uses
Arcen single-thread — Rc es más ligero
🏗️ Metodología
- Un único propietario: valor directo o
Box - Múltiples propietarios (single-thread):
Rc - Múltiples propietarios (multi-thread):
Arc - Mutabilidad interior:
Cell(Copy),RefCell(no-Copy),Mutex(threads)
🔗 Para saber más
POO y patrones de diseño
Structs + impl (composición sobre herencia)
¿Qué es?
Rust no tiene clases ni herencia. En su lugar, usa structs para datos e impl blocks para métodos, con composición como alternativa a la herencia. Los traits proporcionan polimorfismo sin jerarquías de clases.
Sintaxis básica
struct Multimedia {
nombre: String,
duracion: u32,
}
impl Multimedia {
fn new(nombre: &str, duracion: u32) -> Self {
Self { nombre: nombre.to_string(), duracion }
}
fn describir(&self) -> String {
format!("{} ({}s)", self.nombre, self.duracion)
}
}
// Composición en lugar de herencia
struct Video {
base: Multimedia,
resolucion: (u32, u32),
}
struct Audio {
base: Multimedia,
bitrate: u32,
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
struct Punto { x: i32, y: i32 }
impl Punto {
fn new(x: i32, y: i32) -> Self { Self { x, y } }
fn distancia(&self, otro: &Punto) -> f64 {
((self.x - otro.x).pow(2) + (self.y - otro.y).pow(2)) as f64
}
}
#[test]
fn test_struct_methods() {
let a = Punto::new(0, 0);
let b = Punto::new(3, 4);
assert_eq!(a.distancia(&b), 5.0);
}
}
💡 Memoria y rendimiento
- Composición sin herencia: no hay vtable overhead (como en C++)
- Layout lineal en memoria (más cache-friendly que jerarquías con herencia)
implblocks se compilan a funciones estáticas: cero overhead
✅ Buenas prácticas
- ✅ Prefiere composición sobre herencia
- ✅ Usa traits para definir comportamiento compartido en lugar de clases base
- ⚠️ No intentes emular herencia con structs anidados a menos que sea necesario
- ❌ No crees jerarquías profundas con composición — refactoriza en traits
🏗️ Metodología
- Structs planos: datos + métodos (90% de los casos)
- Composición: struct A contiene struct B (relación “tiene un”)
- Traits: comportamiento compartido entre tipos no relacionados
🔗 Para saber más
Patrones de diseño en Rust
¿Qué es?
Los patrones de diseño se adaptan al paradigma del lenguaje. Rust tiene implementaciones idiomáticas para los patrones clásicos, aprovechando ownership, traits y enums.
| Patrón | En Rust | Ejemplo |
|---|---|---|
| Strategy | Funciones/closures + traits | Box<dyn Fn()> o genéricos |
| Observer | Canales (std::sync::mpsc) o callbacks | tokio::sync::broadcast |
| Factory | Funciones constructoras o From trait | T::new(), From<T> |
| Singleton | No recommended; módulos como singleton | once_cell::sync::Lazy |
| Builder | Struct builder con métodos chain | struct ConfigBuilder |
| Adapter | Wrapper struct + From/Into traits | struct MiTipo(Wrapped) |
Sintaxis básica: Builder
#[derive(Debug)]
struct Config {
host: String,
puerto: u16,
timeout: u32,
}
struct ConfigBuilder {
host: String,
puerto: u16,
timeout: u32,
}
impl ConfigBuilder {
fn new() -> Self {
Self { host: "localhost".into(), puerto: 8080, timeout: 30 }
}
fn host(mut self, host: &str) -> Self { self.host = host.into(); self }
fn puerto(mut self, puerto: u16) -> Self { self.puerto = puerto; self }
fn build(self) -> Config {
Config { host: self.host, puerto: self.puerto, timeout: self.timeout }
}
}
let config = ConfigBuilder::new()
.host("example.com")
.puerto(443)
.build();
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
trait Estrategia { fn ejecutar(&self, a: i32, b: i32) -> i32; }
struct Suma;
impl Estrategia for Suma { fn ejecutar(&self, a: i32, b: i32) -> i32 { a + b } }
struct Resta;
impl Estrategia for Resta { fn ejecutar(&self, a: i32, b: i32) -> i32 { a - b } }
fn calcular(estrategia: &dyn Estrategia, a: i32, b: i32) -> i32 {
estrategia.ejecutar(a, b)
}
#[test]
fn test_strategy_pattern() {
assert_eq!(calcular(&Suma, 10, 5), 15);
assert_eq!(calcular(&Resta, 10, 5), 5);
}
#[test]
fn test_builder() {
#[derive(Debug, PartialEq)]
struct Conf { host: String, port: u16 }
let c = Conf { host: "test".into(), port: 80 };
assert_eq!(c, Conf { host: "test".into(), port: 80 });
}
}
💡 Memoria y rendimiento
- Builder pattern: cero overhead si usa tipos concretos (no Box)
- Strategy con genéricos: monomorfización, máximo rendimiento
- Strategy con
dyn Fn: indirección de vtable, overhead mínimo - Los patrones en Rust suelen resolverse con tipos concretos y traits
✅ Buenas prácticas
- ✅ Usa el builder pattern para constructores con muchos parámetros
- ✅ Prefiere
From/Intotraits sobrenew()conversiones - ⚠️ Singleton no es idiomático en Rust — usa módulos con lazy_static o once_cell
- ❌ No implements Factory pattern con
Box<dyn>a menos que necesites heterogeneidad
🔗 Para saber más
Polimorfismo en detalle
Polimorfismo paramétrico (genéricos)
¿Qué es?
Rust ofrece polimorfismo paramétrico vía genéricos. A diferencia de Java (erasure), Rust monomorfiza los genéricos: genera código máquina específico para cada combinación de tipos usada.
Sintaxis básica
// Función genérica
fn el_mayor<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Struct genérico
struct Par<T, U> { primero: T, segundo: U }
// impl genérico
impl<T> Par<T, T> {
fn ambos(self) -> (T, T) { (self.primero, self.segundo) }
}
// Const genéricos (Rust 1.51+)
fn suma_array<const N: usize>(arr: &[i32; N]) -> i32 {
arr.iter().sum()
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
fn id<T>(x: T) -> T { x }
#[test]
fn test_generic_fn() {
assert_eq!(id(42), 42);
assert_eq!(id("hola"), "hola");
}
#[test]
fn test_generic_struct() {
let p = (1, "a");
assert_eq!(p, (1, "a"));
}
}
💡 Memoria y rendimiento
- Monomorfización: el compilador duplica el código por cada tipo usado
- Sin overhead en runtime: cada instancia tiene su propia implementación
- El tamaño del binario puede crecer —
dyn Traitlo evita (trade-off) - Const genéricos: completamente en compilación, zero-cost
✅ Buenas prácticas
- ✅ Prefiere monomorfización en hot paths para máximo rendimiento
- ✅ Usa
constgenéricos para arrays de tamaño fijo - ⚠️ La monomorfización puede inflar el binario — agrupa tipos similares si es crítico
- ❌ No pongas demasiados bounds en genéricos sin necesidad
🔗 Para saber más
Polimorfismo ad-hoc (traits)
¿Qué es?
El polimorfismo ad-hoc permite que diferentes tipos respondan a la misma interfaz de formas diferentes. En Rust se implementa con traits y operator overloading.
Sintaxis básica
use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Punto { x: i32, y: i32 }
impl Add for Punto {
type Output = Punto;
fn add(self, other: Punto) -> Punto {
Punto { x: self.x + other.x, y: self.y + other.y }
}
}
// Overloading con operador +
let a = Punto { x: 1, y: 2 };
let b = Punto { x: 3, y: 4 };
let c = a + b; // usa nuestro impl Add
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Metros(f64);
impl Add for Metros {
type Output = Metros;
fn add(self, other: Metros) -> Metros { Metros(self.0 + other.0) }
}
#[test]
fn test_operator_overloading() {
assert_eq!(Metros(10.0) + Metros(5.0), Metros(15.0));
}
}
💡 Memoria y rendimiento
- Operator overloading via traits: se resuelve en compilación (monomorfización)
- Igual de eficiente que una función manual
Copy+Addevita clonaciones innecesarias
✅ Buenas prácticas
- ✅ Implementa
Add,Mul, etc. para tipos matemáticos (vectores, matrices) - ⚠️ No abuses del operator overloading — si no es obvio, usa métodos con nombre
- ❌ No implementes
Addpara tipos donde+no tenga sentido semántico
🔗 Para saber más
Interacción con contenido multimedia
image crate (PNG, JPEG, WebP, GIF)
¿Qué es?
La crate image es la librería estándar de Rust para procesamiento de imágenes. Soporta decenas de formatos, lectura/escritura, redimensionado, recorte y conversión.
Sintaxis básica
use image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"
// Cargar y obtener información
let img = image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"
println!("{}x{}", img.width(), img.height());
// Redimensionar
let redimensionada = img.resize(800, 600, image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"
redimensionada.save("foto_800x600.jpg")?;
// Convertir formato
let img = image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"
img.save_with_format("foto.webp", ImageFormat::WebP)?;
// Recortar
let recortada = img.crop_imm(100, 100, 200, 200);
recortada.save("recorte.png")?;
// Escala de grises
let gris = img.grayscale();
gris.save("gris.jpg")?;
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"
#[test]
fn test_crear_imagen() {
let mut img = RgbImage::new(2, 2);
img.put_pixel(0, 0, Rgb([255, 0, 0]));
img.put_pixel(1, 0, Rgb([0, 255, 0]));
assert_eq!(img.width(), 2);
assert_eq!(img.height(), 2);
}
}
💡 Memoria y rendimiento
- Las imágenes se cargan en memoria como
Vec<u8>sin comprimir - Una imagen 4K (3840x2160) RGBA ocupa ~32 MB en RAM
- El decodificador es streaming para JPEG (línea por línea)
- PNG/WebP se decodifican completamente antes de acceder a los píxeles
✅ Buenas prácticas
- ✅ Usa `image: “img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp”
- ✅ Usa
save_with_formatpara controlar el formato de salida - ⚠️ No redimensiones con
Nearestpara reducciones grandes (aliasing) - ❌ No cargues imágenes enormes sin verificar el tamaño primero
🔗 Para saber más
Procesamiento de audio
¿Qué es?
Para procesamiento de audio en Rust, las crates principales son hound (WAV), rodio (reproducción) y symphonia (decodificación de múltiples formatos).
Sintaxis básica
// Leer WAV con hound
use hound::WavReader;
let mut reader = WavReader::open("audio.wav")?;
let spec = reader.spec();
println!("{:?}", spec); // 44100 Hz, 16 bits, stereo
// Acceder a muestras
let muestras: Vec<i16> = reader.samples::<i16>()
.filter_map(|s| s.ok())
.collect();
// Reproducir con rodio
use rodio::{OutputStream, Source, source::SineWave};
let (_stream, handle) = OutputStream::try_default()?;
let source = SineWave::new(440.0); // La A4
handle.play_raw(source.convert_samples());
std::thread::sleep(std::time::Duration::from_secs(1));
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use hound::{WavSpec, WavWriter, WavReader};
use std::io::Cursor;
#[test]
fn test_write_read_wav() {
let spec = WavSpec {
channels: 1, sample_rate: 44100,
bits_per_sample: 16, sample_format: hound::SampleFormat::Int,
};
let mut buf = Cursor::new(Vec::new());
{
let mut writer = WavWriter::new(&mut buf, spec).unwrap();
for s in 0..100 { writer.write_sample(s as i16).unwrap(); }
}
buf.set_position(0);
let reader = WavReader::new(buf).unwrap();
assert_eq!(reader.len(), 100);
}
}
💡 Memoria y rendimiento
- WAV sin comprimir: ~10 MB/minuto a 44100 Hz/16 bits/stereo
- hound lee/escribe muestras en streaming: no carga todo en memoria
- rodio usa buffers internos para reproducción continua
✅ Buenas prácticas
- ✅ Usa hound para leer/escribir WAV directamente
- ✅ Usa rodio para reproducción en tiempo real
- ⚠️ El audio en tiempo real necesita manejo cuidadoso de buffers (evita underruns)
- ❌ No proceses audio en el hilo principal de UI — usa un hilo separado
🔗 Para saber más
Bases de datos
Diesel (ORM)
¿Qué es?
Diesel es el ORM más maduro de Rust. Es seguro en tipos, generación de esquemas en compilación, y compatible con PostgreSQL, MySQL y SQLite. Define la estructura de la base de datos en Rust y genera consultas verificadas en compilación.
Sintaxis básica
[dependencies]
diesel = { version = "2", features = ["postgres"] }
diesel_cli = { version = "2", features = ["postgres"] }
// schema.rs (generado por diesel_cli)
table! {
usuarios (id) {
id -> Int4,
nombre -> Varchar,
email -> Varchar,
activo -> Bool,
}
}
// models.rs
#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::usuarios)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct Usuario {
id: i32,
nombre: String,
email: String,
activo: bool,
}
// Consultas
use crate::schema::usuarios::dsl::*;
let results = usuarios.filter(activo.eq(true))
.limit(10)
.load::<Usuario>(&mut conn)?;
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_query_builder() {
use diesel::prelude::*;
let query = diesel::dsl::select(1.into_sql::<diesel::sql_types::Integer>());
assert!(query.is_some());
}
}
💡 Memoria y rendimiento
- Diesel usa query builders en compilación — cero overhead en runtime
- Las consultas se verifican contra el esquema en compilación
- La conexión a BD es pooling: reusa conexiones para evitar overhead de handshake
✅ Buenas prácticas
- ✅ Usa
diesel_clipara generar schema.rs automáticamente - ✅ Usa migrations para cambios de esquema versionados
- ⚠️ No exponer queries SQL crudas sin verificar — usa el DSL siempre que puedas
- ❌ No compartas una conexión entre threads sin pool
🔗 Para saber más
SQLx (async, compile-time checked)
¿Qué es?
SQLx es un crate async para SQL que verifica las queries en compilación (contra una BD real o con un archivo de datos). Soporta PostgreSQL, MySQL, SQLite. No es ORM: escribes SQL directamente.
Sintaxis básica
use sqlx::postgres::PgPool;
let pool = PgPool::connect("postgres://user:pass@localhost/db").await?;
// Query verificada en compilación
let usuarios: Vec<(i32, String)> = sqlx::query_as(
"SELECT id, nombre FROM usuarios WHERE activo = $1"
)
.bind(true)
.fetch_all(&pool)
.await?;
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use sqlx::PgPool;
#[sqlx::test]
async fn test_insert(pool: PgPool) -> sqlx::Result<()> {
sqlx::query("INSERT INTO test (val) VALUES ($1)")
.bind(42)
.execute(&pool)
.await?;
Ok(())
}
}
💡 Memoria y rendimiento
- SQLx usa connection pooling por defecto: reusa conexiones
- Las queries verificadas en compilación tienen costo de compilación adicional
- En runtime: mínimo overhead, similar a drivers nativos
✅ Buenas prácticas
- ✅ Prefiere SQLx para proyectos async (Tokio/Axum)
- ✅ Usa
sqlx::testpara tests con BD real o docker - ⚠️ Las queries verificadas en compilación necesitan
DATABASE_URLen build - ❌ No mezcles ORM y queries crudas sin una capa de abstracción
🔗 Para saber más
WebSockets y mensajería
tokio-tungstenite (WebSocket)
¿Qué es?
tokio-tungstenite proporciona soporte WebSocket asíncrono sobre Tokio. Permite comunicación bidireccional en tiempo real entre cliente y servidor.
Sintaxis básica
use tokio_tungstenite::connect_async;
use futures_util::StreamExt;
let url = url::Url::parse("ws://echo.websocket.org").unwrap();
let (ws_stream, _) = connect_async(url).await?;
let (_, mut read) = ws_stream.split();
while let Some(msg) = read.next().await {
let msg = msg?;
if let tokio_tungstenite::tungstenite::Message::Text(text) = msg {
println!("recibido: {text}");
}
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_websocket_url() {
let url = url::Url::parse("ws://localhost:8080/ws").unwrap();
assert_eq!(url.scheme(), "ws");
assert_eq!(url.host_str(), Some("localhost"));
}
}
💡 Memoria y rendimiento
- Las conexiones WebSocket son persistentes: sin overhead de handshake por mensaje
- tokio-tungstenite maneja miles de conexiones concurrentes con una sola tarea por conexión
- El overhead por conexión es ~2-4 KB (TCP buffer + estructura de estado)
✅ Buenas prácticas
- ✅ Usa WebSocket para comunicación en tiempo real (chats, notificaciones, streaming)
- ⚠️ Siempre maneja reconexión y heartbeats
- ❌ No uses WebSocket para APIs REST tradicionales — HTTP es más simple y cacheable
🔗 Para saber más
Concurrencia y paralelismo
Tokio: async/await
¿Qué es?
Tokio es el runtime asíncrono de Rust. Proporciona un scheduler work-stealing para tareas async, E/S no bloqueante, timers, canales y sincronización. Es el estándar de facto para aplicaciones de red.
Sintaxis básica
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let tarea1 = tokio::spawn(async {
sleep(Duration::from_millis(100)).await;
"tarea 1"
});
let tarea2 = tokio::spawn(async {
sleep(Duration::from_millis(50)).await;
"tarea 2"
});
let r1 = tarea1.await.unwrap();
let r2 = tarea2.await.unwrap();
println!("{r1} y {r2}");
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_async() {
let result = tokio::spawn(async { 42 }).await.unwrap();
assert_eq!(result, 42);
}
#[tokio::test]
async fn test_sleep() {
let start = std::time::Instant::now();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
assert!(start.elapsed() >= std::time::Duration::from_millis(10));
}
}
💡 Memoria y rendimiento
- Tokio usa un scheduler work-stealing: distribuye tareas entre hilos del thread pool
- Cada tarea async ocupa ~1-2 KB de overhead (frente a ~8 KB de un hilo OS)
- El cambio de contexto es ~0.1 µs (frente a ~1 µs para hilos de OS)
- Para tareas CPU-bound, usa
spawn_blocking(no bloquees el pool async)
✅ Buenas prácticas
- ✅ Usa
#[tokio::main]para entry points async - ✅ Usa
tokio::spawnpara tareas concurrentes - ⚠️ No bloquees tareas con
std::thread::sleep— usatokio::time::sleep - ❌ No mezcles async y sync sin
spawn_blocking
🏗️ Metodología
- I/O bound (red, disco): async con Tokio
- CPU bound (cálculo pesado):
spawn_blockingo hilos separados - Mixto: async con canales a workers CPU-bound
🔗 Para saber más
Rayon: paralelismo CPU-bound
¿Qué es?
Rayon convierte fácilmente iteraciones secuenciales en paralelo con .par_iter(). Usa un thread pool work-stealing para dividir el trabajo automáticamente.
Sintaxis básica
use rayon::prelude::*;
fn main() {
let nums: Vec<u64> = (0..1_000_000).collect();
// Secuencial
let suma: u64 = nums.iter().sum();
// Paralelo (cambia .iter() por .par_iter())
let suma_par: u64 = nums.par_iter().sum();
// Map-reduce paralelo
let resultado: u64 = (0..1_000_000u64)
.into_par_iter()
.map(|x| x * x)
.sum();
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use rayon::prelude::*;
#[test]
fn test_par_iter() {
let v: Vec<i32> = (0..100).collect();
let sum: i32 = v.par_iter().sum();
assert_eq!(sum, 4950);
}
}
💡 Memoria y rendimiento
- Rayon divide el trabajo en chunks y los distribuye entre hilos
- El thread pool se inicializa una vez: número de hilos = núcleos lógicos
- Overhead de división: ~1 µs por chunk — solo rentable para trabajo significativo
- Recomendado para operaciones CPU-bound pesadas (cálculos, procesamiento de imágenes)
✅ Buenas prácticas
- ✅ Usa
par_iter()para operaciones CPU-bound con colecciones grandes - ✅ Usa
par_sort()en lugar desort()para vectores grandes (>10K elementos) - ⚠️ No uses Rayon para E/S (red, archivos) — prefiere Tokio o hilos
- ❌ No anides par_iter() dentro de par_iter() — el pool se satura
🔗 Para saber más
Tabla de decisión: threading vs async vs Rayon
| threading | async (Tokio) | Rayon | |
|---|---|---|---|
| Propósito | CPU-bound o I/O blocking | I/O-bound no bloqueante | CPU-bound paralelo |
| Overhead por tarea | ~8 KB (pila) | ~1-2 KB | ~0 (pool reusado) |
| Context switch | ~1 µs | ~0.1 µs | N/A (work-stealing) |
| Escala | Cientos | Cientos de miles | Núcleos de CPU |
| Facilidad | Manual (join, sync) | Fácil (async/await) | Muy fácil (par_iter) |
| Uso típico | Cálculo pesado, FFI | Servidores web, BD | Procesamiento de datos |
Testing y calidad
Frameworks de testing en Rust
Rust tiene soporte nativo de testing con #[test]. Además, existen herramientas especializadas:
| Framework | Propósito | Async? | CLI |
|---|---|---|---|
#[test] (built-in) | Unit tests | No (necesita #[tokio::test]) | cargo test |
cargo test | Integración + doc tests | Sí | cargo test --test |
proptest | Property-based testing | No | cargo test |
criterion | Benchmarks | No | cargo bench |
tarpaulin | Cobertura | Sí | cargo tarpaulin |
Sintaxis básica
// Unit test
pub fn suma(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_suma() {
assert_eq!(suma(2, 2), 4);
}
#[test]
fn test_suma_negativos() {
assert_eq!(suma(-1, 1), 0);
}
#[test]
#[should_panic(expected = "desbordamiento")]
fn test_panic() {
panic!("desbordamiento");
}
}
// Integration test (tests/integration.rs)
use mi_crate::suma;
#[test]
fn test_integracion() {
assert_eq!(suma(2, 2), 4);
}
// Doc test
/// ```
/// let r = mi_crate::suma(2, 2);
/// assert_eq!(r, 4);
/// ```
pub fn suma(a: i32, b: i32) -> i32 { a + b }
🧪 Cómo probarlo
# Ejecutar todos los tests
cargo test
# Tests específicos
cargo test test_suma
cargo test tests::test_suma -- --nocapture
# Con filtro
cargo test --test integration_test
cargo test --lib
# Doc tests
cargo test --doc
# Benchmarks (criterion)
cargo bench
# Cobertura
cargo install cargo-tarpaulin
cargo tarpaulin --out Html
💡 Memoria y rendimiento
- Los tests se ejecutan en paralelo por defecto (
cargo test -- --test-threads=1para serial) - Doc tests verifican que los ejemplos en la documentación funcionan
- Los tests de integración van en
tests/y son archivos separados del crate - Cada test recibe un hilo separado; el overhead es mínimo
✅ Buenas prácticas
- ✅ Escribe tests para toda función pública
- ✅ Usa doc tests para documentación con ejemplos ejecutables
- ⚠️ No dejes
println!en tests que ejecutas en CI (usa--nocapturesolo localmente) - ❌ No escribas tests que dependan del orden de ejecución
🏗️ Metodología de testing
- Unit tests: junto al código (
#[cfg(test)] mod tests) - Integration tests: en
tests/— prueban el crate como API pública - Doc tests: documentación con ejemplos ejecutables (siempre funcionan)
- Property-based: con
proptestpara verificar invariantes con datos aleatorios - Fuzzing: con
cargo-fuzzpara testing de seguridad
🔗 Para saber más
Conceptos avanzados
unsafe, FFI y no_std
¿Qué es?
unsafe permite operaciones que el borrow checker no puede verificar: desreferenciar punteros crudos, llamar a funciones C (FFI), acceder a static mut, e implementar traits unsafe. no_std elimina la stdlib para entornos embebidos.
Sintaxis básica
// unsafe: desreferenciar puntero crudo
let x = 42;
let r = &x as *const i32;
unsafe {
println!("{}", *r);
}
// FFI: llamar a C
extern "C" {
fn abs(x: i32) -> i32;
}
unsafe { println!("{}", abs(-5)); }
// no_std (para embebido)
// #![no_std]
// use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[test]
fn test_ffi_wrapper() {
extern "C" fn suma_c(a: i32, b: i32) -> i32 { a + b }
let result = unsafe { suma_c(2, 3) };
assert_eq!(result, 5);
}
}
💡 Memoria y rendimiento
unsafeno desactiva el borrow checker — solo permite operaciones adicionales- FFI tiene overhead de llamada (ABI C) ~5-10 ns
- no_std elimina ~100 KB del binario para entornos embebidos
- Las llamadas FFI no pueden ser inlineadas
✅ Buenas prácticas
- ✅ Minimiza unsafe: encapsúlalo en funciones seguras
- ✅ Usa
#[no_mangle]yextern "C"para exportar funciones a C - ⚠️ unsafe no significa “sin checks” — documenta cada invariante
- ❌ No uses unsafe sin una razón muy concreta y documentada
🔗 Para saber más
Pin y async traits
¿Qué es?
Pin garantiza que un valor no se mueva en memoria, necesario para self-referential structs en async. Los async traits (Rust 1.75+) permiten métodos async en traits, eliminando la necesidad de cajas como async-trait.
Sintaxis básica
use std::pin::Pin;
// Pin: valor que no se moverá
let mut dato = Box::new(42);
let pinned = Pin::new(&mut dato);
// Async trait (Rust 1.75+)
#[async_trait]
trait Servicio {
async fn obtener(&self) -> String;
}
struct MiServicio;
#[async_trait]
impl Servicio for MiServicio {
async fn obtener(&self) -> String {
"datos".to_string()
}
}
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_async_trait() {
trait Simple {
async fn get(&self) -> i32;
}
struct X;
impl Simple for X {
async fn get(&self) -> i32 { 42 }
}
assert_eq!(X.get().await, 42);
}
}
💡 Memoria y rendimiento
- Pin es un wrapper de compilación: cero overhead en runtime
- self-referential structs ocupan el tamaño del valor + padding
- async traits generan state machines por implementación: monomorfización
✅ Buenas prácticas
- ✅ Usa async traits nativos (Rust 1.75+) en lugar de
async-traitcrate - ✅ Pin es interno a los generadores async — no necesitas usarlo directamente
- ⚠️ No implementes tu propio Pin a menos que estés haciendo unsafe
- ❌ No muevas un valor después de haberlo pinned
🔗 Para saber más
WASM (WebAssembly)
¿Qué es?
Rust compila a WebAssembly, permitiendo ejecutar código de alto rendimiento en el navegador. Con wasm-pack puedes empaquetar y publicar en npm.
Sintaxis básica
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
// En JavaScript:
// import { fibonacci } from './pkg';
// console.log(fibonacci(10));
🧪 Cómo probarlo
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn test_fib() {
assert_eq!(super::fibonacci(10), 55);
}
}
💡 Memoria y rendimiento
- WASM ejecuta en una VM sandboxed dentro del navegador
- El binario WASM suele ser 10-50 KB para aplicaciones pequeñas
- WASM no tiene acceso directo a DOM (necesita JS glue)
- Las llamadas WASM ↔ JS tienen overhead de ~1-10 µs
✅ Buenas prácticas
- ✅ Usa wasm-pack para build, test y publish
- ✅ Prefiere WASM para código CPU-bound (cálculos, juegos, procesamiento)
- ⚠️ No uses WASM para tareas triviales de DOM — JS es más rápido para eso
- ❌ No compartas grandes estructuras entre JS y WASM sin serialización eficiente
🔗 Para saber más
Proyecto final integrador: Procesador de logs
Un procesador de logs que combina file system, algoritmos, módulos, testing y red.
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::collections::HashMap;
#[derive(Debug, PartialEq)]
enum Nivel { INFO, WARN, ERROR }
#[derive(Debug)]
struct LogEntry {
nivel: Nivel,
mensaje: String,
linea: usize,
}
fn parse_nivel(s: &str) -> Option<Nivel> {
match s {
"INFO" => Some(Nivel::INFO),
"WARN" => Some(Nivel::WARN),
"ERROR" => Some(Nivel::ERROR),
_ => None,
}
}
fn procesar_log<P: AsRef<Path>>(path: P) -> Result<Vec<LogEntry>, String> {
let file = fs::File::open(path).map_err(|e| format!("error abriendo: {e}"))?;
let reader = BufReader::new(file);
reader.lines()
.enumerate()
.filter_map(|(i, line)| {
let line = match line {
Ok(l) => l,
Err(_) => return None,
};
let parts: Vec<&str> = line.splitn(2, "]: ").collect();
if parts.len() < 2 { return None; }
let nivel_str = parts[0].trim_start_matches('[');
let mensaje = parts[1].to_string();
parse_nivel(nivel_str).map(|nivel| LogEntry { nivel, mensaje, linea: i + 1 })
})
.collect::<Vec<_>>()
.into_iter()
.collect::<Result<Vec<_>, _>>()
.or_else(|_| Ok(vec![]))
}
fn reportar(entries: &[LogEntry]) -> HashMap<&str, usize> {
let mut stats: HashMap<&str, usize> = HashMap::new();
for entry in entries {
let key = match entry.nivel {
Nivel::INFO => "INFO",
Nivel::WARN => "WARN",
Nivel::ERROR => "ERROR",
};
*stats.entry(key).or_insert(0) += 1;
}
stats
}
fn main() -> Result<(), String> {
let entries = procesar_log("app.log")?;
let stats = reportar(&entries);
for (nivel, count) in &stats {
println!("{nivel}: {count}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_nivel() {
assert_eq!(parse_nivel("INFO"), Some(Nivel::INFO));
assert_eq!(parse_nivel("DEBUG"), None);
}
#[test]
fn test_procesar_log() {
let path = "test.log";
fs::write(path, "[INFO]: inicio
[ERROR]: fallo
").unwrap();
let entries = procesar_log(path).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].nivel, Nivel::INFO);
assert_eq!(entries[1].nivel, Nivel::ERROR);
fs::remove_file(path).unwrap();
}
#[test]
fn test_reportar() {
let entries = vec![
LogEntry { nivel: Nivel::INFO, mensaje: "ok".into(), linea: 1 },
LogEntry { nivel: Nivel::ERROR, mensaje: "fail".into(), linea: 2 },
LogEntry { nivel: Nivel::INFO, mensaje: "otro".into(), linea: 3 },
];
let stats = reportar(&entries);
assert_eq!(*stats.get("INFO").unwrap(), 2);
assert_eq!(*stats.get("ERROR").unwrap(), 1);
}
}
Despliegue a producción
Build para producción
cargo build --release
El binario optimizado estará en target/release/.
Docker multi-stage
FROM rust:1.85-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM scratch
COPY --from=builder /app/target/release/mi-app .
CMD ["./mi-app"]
Hosting
- Fly.io — despliegue con
flyctl launch - Railway — conecta tu repo y despliega automáticamente
- Shuttle — plataforma serverless para Rust
CI/CD (GitHub Actions)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo build --release
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
Canales y recursos en español
Canales de YouTube
- Midudev — Rust, JavaScript, web, entrevistas
- HolaMundo — Rust, sistemas, tutoriales prácticos
- Programación en Español — Rust, Go, sistemas
- Código Espinoza — Rust, desarrollo de videojuegos
- Ringa Tech — Rust, sistemas, WASM, rendimiento
Comunidades
- r/rust — comunidad global
- Rust en Español (Telegram)
- Discord Rust Lang (ES)
- Rust Es (Twitter/X)
Repositorios GitHub destacados
- rust-unofficial/awesome-rust — lista curada de recursos Rust
- learning-rust — recursos para aprender Rust
- rust-gym — problemas resueltos en Rust
Hacks y tips de productividad
1. cargo-watch (auto-rebuild en cambios)
cargo install cargo-watch
cargo watch -x check -x test
cargo watch -x run # para servidores
2. cargo-expand (ver macros expandidas)
cargo install cargo-expand && cargo expand
3. cargo-edit (gestión de dependencias desde CLI)
cargo add serde --features derive
cargo rm old-crate
cargo upgrade
4. cargo-outdated (dependencias desactualizadas)
cargo install cargo-outdated && cargo outdated
5. cargo-audit (vulnerabilidades de seguridad)
cargo install cargo-audit && cargo audit
6. Rust Analyzer tricks
Alt+Enter(VSCode) → ver code actions (auto-import, add derive, add impl):RustAnalyzerExpandMacroen Neovim → expandir macros- Config
rust-analyzer.checkOnSave.command="clippy"para lints automáticos
7. Replace match pesados con if let
// ❌ Verboso
match opt {
Some(v) => println!("{v}"),
None => {}
}
// ✅ Concisa
if let Some(v) = opt { println!("{v}"); }
8. Unit structs como type-level markers
struct Metros;
struct Segundos;
fn velocidad<T>(d: f64, t: f64) -> f64 { d / t }
// El parámetro T es un marker — no ocupa memoria
9. Debuggear con dbg!
let x = 42;
let y = dbg!(x * 2); // imprime "[src/main.rs:2] x * 2 = 84"
10. Testeo de errores con #[should_panic]
#[test]
#[should_panic(expected = "índice fuera de rango")]
fn test_panico() {
let v = vec![1, 2, 3];
let _ = v[10];
}
11. Perfilamiento con flamegraphs
cargo install flamegraph
cargo flamegraph --bin mi-app
12. Profile-guided optimization (PGO)
# Compila con instrumentación
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release
# Ejecuta con datos reales
./target/release/mi-app
# Recompila con los datos de perfil
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data" cargo build --release
⚠️ Errores comunes
Estos son los errores de compilación que más se repiten al empezar con Rust. El borrow checker y el sistema de tipos son estrictos, pero sus mensajes suelen indicar exactamente dónde y cómo arreglarlo.
| Error | Causa | Solución |
|---|---|---|
error[E0308]: mismatched types | Pasas o devuelves un tipo distinto al esperado | Revisa los tipos declarados y convierte o adapta el valor (.into(), as, etc.) |
error[E0382]: use of moved value | Usas una variable después de moverla con vec!/String etc. | Clona el valor (clone()) o haz referencia con & en vez de pasarlo por valor |
error[E0502]: cannot borrow X as immutable because it is also borrowed as mutable | Tienes un &mut y un & del mismo dato a la vez | Separa los accesos en bloques o usa clone() para romper la doble dependencia |
error[E0599]: no method named 'x' found | Llamas a un método que no existe o no está en ámbito (falta use) | Importa el trait con use o revisa el nombre del método y del tipo |
error: linker 'cc' not found | Falta un enlazador o toolchain de C en tu sistema | Instala las build tools (en Linux build-essential, en macOS Xcode CLT, en Windows MSVC) |
the msvc target is not available | Instalaste el toolchain GNU y necesitas el MSVC en Windows | rustup target add x86_64-pc-windows-msvc o instala el compilador con rustup toolchain install stable-msvc |
cargo: command not found | Cargo no está en el PATH o Rust no está instalado | Instala/repasa rustup y añade ~/.cargo/bin al PATH; verifica con rustup --version |
error: failed to run custom build command | Una dependencia con bindings de C falla al compilar | Instala las dependencias del sistema (librerías de desarrollo) o la herramienta que pide la crate |
🔗 Guías relacionadas
- Para empaquetar tu binario Rust en contenedores (build multi-stage): Guía de Docker: De 0 a 100.
- Primeros pasos con contenedores: Instalación de Docker y primeros pasos.
Referencias y documentación oficial
- Rust Programming Language (oficial)
- Rust Book (The Book)
- Rust by Example
- Rustonomicon
- Rust Reference
- Rust Edition Guide
- Cargo Book
- Rust API Guidelines
- Rust Design Patterns
- Tokio Tutorial
- Rust WASM Book
- Rust Embedded Book
- Rust Cookbook
- Awesome Rust (GitHub)
- This Week in Rust (newsletter)
- Rust Blog (oficial)
- Inside Rust Blog
- Rust Playground
- crates.io
- docs.rs
