Guía completa de Rust para desarrolladores: propiedad, concurrencia y rendimiento

rust Guía completa de Rust para desarrolladores: propiedad, concurrencia y rendimiento

Guía completa de Rust para desarrolladores: propiedad, concurrencia y rendimiento

Esta guía práctica reúne conceptos clave, patrones y ejemplos completos para escribir código Rust seguro y rápido. Cobertura: propiedad y borrow checker, lifetimes, manejo de errores, async/concurrency, optimización de memoria, unsafe/FFI, herramientas y estructura de proyecto. Código completo y explicación del por qué en cada sección.

Estructura del proyecto (recomendada)

mi_proyecto_rust/
├─ Cargo.toml
├─ src/
│  ├─ main.rs          // binario
│  ├─ lib.rs           // lógica reutilizable y tests
│  ├─ ownership.rs     // ejemplos de propiedad y borrow
│  ├─ errors.rs        // manejo de errores
│  ├─ async_mod.rs     // ejemplos async con tokio
│  └─ ffi.rs           // ejemplo mínimo FFI
├─ benches/            // benchmarks (criterion)
└─ Cargo.lock

Separar la lógica en lib.rs facilita testing y reuso. Mantén main.rs para orquestar.

1) Propiedad, préstamos y lifetimes (por qué importa)

Rust elimina gran parte de las clases de errores relacionados con memoria en tiempo de compilación. Comprender ownership/borrowing evita clones innecesarios y operaciones costosas.

Ejemplo: mover vs referenciar

// src/ownership.rs
pub fn move_example() {
    let s = String::from("hola");
    let s2 = s; // move: s ya no es válido
    // println!("{}", s); // error de compilación
    println!("{}", s2);
}

pub fn borrow_example() {
    let s = String::from("hola");
    let len = calculate_len(&s); // préstamo inmutable
    println!("longitud: {}", len);
}

fn calculate_len(s: &String) -> usize {
    s.len()
}

Por qué: usar préstamos evita clones y copias. Prefiere referencias inmutables cuando solo consultas datos.

Lifetimes básicos

// caso simple de lifetimes en src/ownership.rs
pub fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Uso:
// let r;
// {
//     let s1 = String::from("longitud larga");
//     let s2 = "corta";
//     r = longest(s1.as_str(), s2);
// }
// println!("{}", r); // error si r viviera más que s1

Por qué: declarar lifetimes hace explícita la relación de alcance entre referencias y evita dangling pointers.

2) Manejo de errores: Result, thiserror y anyhow

Usa Result y enum de errores para API pública (detallados). Para aplicaciones CLI, anyhow y Context agilizan el manejo. Para librerías, define errores con thiserror.

// src/errors.rs
use thiserror::Error;
use std::io;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("IO error: {0}")]
    Io(#[from] io::Error),

    #[error("Parsing error: {0}")]
    Parse(String),
}

pub fn might_fail(path: &str) -> Result {
    let s = std::fs::read_to_string(path)?; // convierte io::Error -> AppError::Io
    if s.is_empty() { Err(AppError::Parse("vacío".into())) } else { Ok(s) }
}

Por qué: modelos explícitos de error facilitan pruebas y manejo en capas superiores. Evita unwrap() en producción.

3) Async en Rust (Tokio)

Para I/O concurrido, usa Tokio + async/await. Evita bloqueos (blocking) en runtimes async.

// src/async_mod.rs
use tokio::time::{sleep, Duration};

pub async fn worker(id: usize) {
    println!("Worker {} iniciando", id);
    sleep(Duration::from_millis(100 * id as u64)).await;
    println!("Worker {} terminado", id);
}

pub async fn run_concurrent() {
    let handles: Vec<_> = (1..=5).map(|i| tokio::spawn(worker(i))).collect();
    for h in handles {
        let _ = h.await; // propaga posibles panics del task
    }
}

Consejo: utiliza tokio::spawn para tareas CPU-limitadas delega a spawn_blocking o thread-pools.

4) Concurrencia segura: Send y Sync

Tipos que cruzan threads deben implementar Send. Acceso concurrente a datos mutables requiere primitivas: Mutex, RwLock o canales (mpsc).

use std::sync::{Arc, Mutex};

let data = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..4).map(|_| {
    let d = Arc::clone(&data);
    std::thread::spawn(move || {
        let mut v = d.lock().unwrap();
        *v += 1;
    })
}).collect();

Por qué: combinar Arc (conteo atómico) y Mutex es el patrón estándar para estados compartidos entre hilos.

5) Optimización de memoria y rendimiento

Pequeños cambios marcan diferencia: evitar clones, reservar capacidad, preferir iteradores, medir antes de optimizar.

  • Usa Vec::with_capacity cuando conozcas tamaño aproximado.
  • Evita String <-> &str conversions innecesarias.
  • Prefiere iter() y collect() en lugar de loops manuales cuando el optimizador puede vectorizar.
  • Comprende layout: repr(C) para FFI, agrupación de campos para evitar padding.
// ejemplo: evitar clones
fn join_words(words: Vec) -> String {
    words.join(" ") // consume el vector
}

Herramientas: cargo bench (criterion), perf + flamegraph, cargo-llvm-lines para ver hot spots.

6) Uso de unsafe: cuándo y cómo

No uses unsafe a la ligera. Motivos válidos: FFI, optimizaciones críticas, acceder a APIs de bajo nivel. Encapsula unsafe detrás de APIs seguras y prueba rigurosamente.

// ejemplo mínimo: leer memoria sin inicializar (solo demo)
unsafe fn read_uninit() -> i32 {
    use std::mem::MaybeUninit;
    let mut x: MaybeUninit = MaybeUninit::uninit();
    // inicializar manualmente
    x.as_mut_ptr().write(42);
    x.assume_init()
}

Reglas: documenta invariantes, limita el alcance de bloques unsafe, revisa con colegas.

7) FFI con C (ejemplo mínimo)

// src/ffi.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

// Para usar desde C: genera cdylib y llama add

En Cargo.toml usa crate-type = ["cdylib"] para crear una librería enlazable.

8) Pruebas y benchmarks

// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_add() {
        assert_eq!(add(2,3), 5);
    }
}

Para micro-benchmarks usa criterion (más fiable que bench estable). Integra en CI para detectar regresiones.

9) Herramientas útiles

  • rustfmt: formato consistente
  • clippy: recomendaciones idiomáticas
  • cargo-audit: detecta vulnerabilidades de dependencias
  • cargo-deny: políticas de licencia y vetado
  • rust-analyzer: autocompletado y navegación

10) CI básica con GitHub Actions

name: Rust CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          profile: minimal
      - name: Cache cargo
        uses: actions/cache@v3
        with:
          path: ~/.cargo/registry
          key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
      - name: Run tests
        run: cargo test --all
      - name: Run clippy
        run: cargo clippy --all -- -D warnings

Checklist de mejores prácticas

  • Evita unwrap() en librerías: propaga errores
  • Prefiere referencias y borrows sobre clones
  • Usa crates maduras (tokio, serde, anyhow, thiserror)
  • Encapsula unsafe y documenta invariantes
  • Escribe tests unitarios y de integración
  • Mide antes de optimizar: bench + profiler

Ejemplo completo mínimo: CLI que lee un archivo y procesa líneas async

// Cargo.toml (extracto)
// [package]
// name = "mi_proyecto_rust"
// edition = "2021"
//
// [dependencies]
// tokio = { version = "1", features = ["full"] }
// anyhow = "1"
// thiserror = "1"

// src/main.rs
use anyhow::Context;
mod async_mod;
mod errors;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let path = std::env::args().nth(1).context("falta ruta al archivo")?;
    let s = tokio::fs::read_to_string(&path).await.context("leer archivo")?;
    for line in s.lines() {
        tokio::spawn(async move {
            // proceso por línea (simulado)
            println!("procesando: {}", line);
        });
    }
    // espera simple (en real usa JoinHandle o un contador)
    tokio::time::sleep(std::time::Duration::from_millis(200)).await;
    Ok(())
}

Por qué: ejemplo ilustra lectura async + spawn de tareas. En producción recoge los JoinHandle para propagar errores o usa FuturesUnordered para control fino.

Recursos y siguientes pasos

  • Libro oficial: The Rust Programming Language (disponible gratis)
  • Rust By Example
  • Repositorios de proyectos reales para estudiar patterns

Advertencia avanzada: no confíes solo en compilación exitosa para garantizar corrección en concurrencia y unsafe. Usa herramientas dinámicas (sanitizers), pruebas de propiedad (proptest) y revisiones de código para invariantes críticas.

Siguiente paso sugerido: crea un micro-servicio en Rust que exponga una API HTTP (axum/hyper), instrumenta con metrics + tracing y perfila con flamegraph para identificar cuellos de botella.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión