Saltar al contenido principal
75 min de lectura1 de agosto de 2026
RustSistemasOwnershipCargoRendimiento

Requisitos previos

  • c-lang

Lo que aprenderás

  • Entender ownership, borrowing y lifetimes
  • Modelar datos con structs, enums y match
  • Implementar traits y derive
  • Manejar errores con Result y Option
  • Crear y compilar proyectos con Cargo
Tutorial75 min

Rust: memoria segura y rendimiento

Rust te da el rendimiento de C sin los dolores de cabeza de la gestión manual de memoria. A través de su modelo de ownership, el compilador elimina errores de memoria en tiempo de compilación. Ideal si ya conoces punteros en C y quieres escribir código rápido sin garbage collector.


Qué es Rust

Rust nació en 2006 como un proyecto personal de Graydon Hoare en Mozilla y se hizo público en 2010. La motivación era construir un lenguaje de sistemas seguro: sin garbage collector, sin fugas de memoria y sin accesos a memoria no válida, pero con el rendimiento de C/C++.

La clave es su modelo de ownership: el compilador rastrea en tiempo de compilación quién posee cada dato, cuándo se libera y cuándo se puede acceder a él. Si tu código tiene un use-after-free, un doble free o una carrera de datos, no compila. Errores que en C causan vulnerabilidades de seguridad aquí se detectan antes de ejecutar nada.

El objetivo de Rust no es que el código "compile a la primera". Es muy normal que el compilador te corrija 20 veces antes de que tu programa haga algo. Esas correcciones son precisamente el valor: el compilador te enseña dónde está el problema de memoria antes de que llegue a producción.

El ecosistema gira alrededor de Cargo, la herramienta oficial que combina gestor de paquetes, build system y runner de tests, similar a lo que npm es para Node pero integrado desde el primer día. El registro de paquetes se llama crates.io y cada paquete se llama crate.

Instalar

La forma oficial de instalar Rust es con rustup, el gestor de toolchains. Se instala con un script y luego te da acceso a cargo (build/gestor) y rustc (compilador):

# Instalar rustup (macOS y Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Recargar la configuración del shell
source "$HOME/.cargo/env"

# Verificar
cargo --version
rustc --version

Crea un proyecto nuevo con cargo new. Te genera la estructura estándar y un repositorio git inicializado:

cargo new mi-proyecto
cd mi-proyecto

# Estructura generada:
#   Cargo.toml   → metadatos y dependencias
#   src/main.rs  → código fuente

# Compilar y ejecutar
cargo run

El Cargo.toml generado es mínimo:

[package]
name = "mi-proyecto"
version = "0.1.0"
edition = "2021"

[dependencies]

Y el main.rs inicial:

fn main() {
    println!("Hello, world!");
}
cargo new inicializa un repositorio git automáticamente. Si estás dentro de un repo ya existente, usa cargo new mi-proyecto --vcs none para evitarlo.

Variables y tipos

Por defecto las variables en Rust son inmutables. Para poder modificarlas hay que marcarlas con mut. Es la decisión más segura: la mutabilidad pasa a ser explícita y solo donde realmente la necesitas:

fn main() {
    let mut contador = 0;   // mutable
    contador += 1;

    let nombre = "Ana";     // inmutable: no se puede reasignar
    // nombre = "Luis";     // ERROR: no se puede asignar a variable inmutable

    println!("{contador} {nombre}");
}

El shadowing permite re-declarar un nombre con un nuevo valor o incluso un nuevo tipo, sin necesidad de mut:

fn main() {
    let x = 5;
    let x = x + 1;   // sombrea la anterior
    let x = x * 2;   // vuelve a sombrear
    println!("{x}"); // 12

    let texto = "42";
    let numero: i32 = texto.parse().expect("no es un número");
}

Los tipos básicos de Rust son explícitos y con tamaños definidos:

let activo: bool = true;        // booleano
let edad: u8 = 30;             // entero sin signo, 8 bits
let saldo: f64 = 99.5;         // coma flotante de 64 bits
let inicial: char = 'A';       // carácter Unicode
let letra = b'B';              // u8 literal de byte

// Tuplas: varios valores de tipos distintos
let par: (i32, &str) = (42, "respuesta");

// Arrays: tamaño fijo
let dias = [1, 2, 3, 4, 5];

// Vectores: dinámicos
let mut numeros = vec![1, 2, 3];
numeros.push(4);

Enteros: i8 a i128 (con signo) y u8 a u128 (sin signo), más isize/usize que dependen de la arquitectura (64 bits en tu máquina). Usa i32 por defecto: es el más rápido en la mayoría de CPU.

Ownership

Esta es la sección más importante de Rust. Cada valor tiene un dueño (owner): la variable que lo contiene. Cuando el dueño sale del ámbito, el valor se libera automáticamente. Las reglas son tres:

// Regla 1: cada valor tiene un único dueño.
// Regla 2: al asignar o pasar como argumento, el valor se MUEVE
//          (el dueño original deja de ser válido).
// Regla 3: cuando el dueño sale del ámbito, el valor se libera.

fn main() {
    let s1 = String::from("hola");
    let s2 = s1;        // s1 se MUEVE a s2

    // println!("{s1}"); // ERROR: el valor fue movido
    println!("{s2}");   // OK
}
En C, hacer s2 = s1 con punteros crea dos punteros al mismo dato: el famoso doble free. En Rust, esa asignación mueve el valor: el compilador impide usar s1 después, eliminando la clase entera de errores. Los tipos que implementan Copy (números, booleanos, chars) se copian en vez de moverse:
fn main() {
    let a = 5;
    let b = a;   // i32 implementa Copy: ambos siguen válidos
    println!("{a} {b}"); // OK: 5 5
}

Para usar un valor sin moverlo, se presta (borrow) con una referencia &. El préstamo inmutable permite leer sin tomar la propiedad:

fn longitud(s: &String) -> usize {
    s.len()   // solo lee, no mueve
}

fn main() {
    let texto = String::from("hello");
    let largo = longitud(&texto); // préstamo inmutable
    println!("{texto} tiene {largo} chars");
}

Si la función necesita modificar el valor, el préstamo debe ser mutable con &mut:

fn anadir_palabra(s: &mut String, palabra: &str) {
    s.push(' ');
    s.push_str(palabra);
}

fn main() {
    let mut saludo = String::from("hola");
    anadir_palabra(&mut saludo, "mundo");
    println!("{saludo}"); // hola mundo
}
El compilador aplica el regla del aliasing: puedes tener muchos préstamos inmutables a la vez, o un único préstamo mutable, pero nunca ambos mezclados. Esto elimina las data races en tiempo de compilación, algo que ningún otro lenguaje de sistemas hace.

Slices y referencias

Un slice es una vista (referencia + longitud) sobre una secuencia de datos contiguos, sin copiarlos. Es la forma idiomática de pasar trozos de texto o listas. El tipo &str es un slice de un String:

fn primera_palabra(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i]; // slice hasta el espacio
        }
    }
    &s[..] // sin espacios: devuelve todo
}

fn main() {
    let frase = String::from("hola mundo");
    let palabra = primera_palabra(&frase);
    println!("{palabra}"); // hola
}

El ejemplo demuestra algo poderoso: la función devuelve una referencia que vive tanto como la entrada. El compilador garantiza que palabra nunca pueda apuntar a memoria ya liberada, aunque frase se modifique después.

Sobre arrays y vectores también puedes crear slices con rangos:

let numeros = [10, 20, 30, 40, 50];
let parte = &numeros[1..3];  // [20, 30]
let todo = &numeros[..];     // [10, 20, 30, 40, 50]
Indexar con un rango fuera de límites pánico en tiempo de ejecución (panic). En Rust, los errores de programación deben fallar rápido y con un mensaje claro en lugar de corromper memoria silenciosamente como en C.

Structs y enums

Los struct agrupan campos, y con impl se les añaden métodos. El &self es un préstamo inmutable del struct (equivale a this):

struct Usuario {
    nombre: String,
    edad: u8,
    activo: bool,
}

impl Usuario {
    // Constructor convencional
    fn nuevo(nombre: String, edad: u8) -> Self {
        Usuario {
            nombre,
            edad,
            activo: true,
        }
    }

    fn saludar(&self) -> String {
        format!("Hola, soy {}", self.nombre)
    }
}

fn main() {
    let ana = Usuario::nuevo(String::from("Ana"), 22);
    println!("{}", ana.saludar());
}

Los enum en Rust son mucho más potentes que en otros lenguajes: cada variante puede llevar datos. Son la base del pattern matching:

enum Estado {
    Activo,
    Pausado { motivo: String }, // variante con campos
    Cancelado(u32),             // variante con un valor
}

fn main() {
    let estado = Estado::Pausado {
        motivo: String::from("vacaciones"),
    };
}

Match y pattern matching

El match es el switch definitivo: cada brazo compara con un patrón y puede extraer datos de la variante. Es exhaustivo: el compilador obliga a cubrir todas las posibilidades:

fn describir(estado: Estado) -> String {
    match estado {
        Estado::Activo => String::from("en marcha"),
        Estado::Pausado { motivo } => format!("pausado por {motivo}"),
        Estado::Cancelado(codigo) => format!("cancelado (código {codigo})"),
    }
}

También funciona sobre números y otros valores, con patrones de rango y comodín _:

fn main() {
    let numero = 3;
    match numero {
        1 => println!("uno"),
        2 | 3 => println!("dos o tres"),
        4..=10 => println!("entre cuatro y diez"),
        _ => println!("otro"),
    }
}

El tipo Option<T> representa un valor que puede existir o no — es la alternativa segura a null. Se combina con match para forzarte a tratar ambos casos:

fn main() {
    let valor: Option<i32> = Some(5);

    match valor {
        Some(n) => println!("Hay un valor: {n}"),
        None => println!("No hay valor"),
    }
}
No existe null en Rust. Donde otros lenguajes usan null (y explotan con NullPointerException), Rust usa Option y te obliga a decidir qué hacer con el caso vacío. Es imposible olvidarse del caso None.

Errores

Rust no tiene excepciones. Los errores recuperables usan Result<T, E>: Ok(T) en éxito o Err(E) en fallo. Se maneja con match o con el operador ?, que propaga el error hacia arriba:

use std::fs::File;
use std::io::{self, ErrorKind};

// Con match, decidiendo caso a caso
fn abrir_o_crear() -> Result<File, io::Error> {
    match File::open("hola.txt") {
        Ok(archivo) => Ok(archivo),
        Err(error) => match error.kind() {
            ErrorKind::NotFound => File::create("hola.txt"),
            otro => Err(otro),
        },
    }
}

El operador ? es azúcar sintáctico: si el Result es Ok, extrae el valor; si es Err, devuelve el error de la función actual. Es la forma idiomática de escribir código limpio sin anidar matches:

use std::fs;
use std::io;

fn leer_contenido(ruta: &str) -> Result<String, io::Error> {
    let contenido = fs::read_to_string(ruta)?; // propaga el error si falla
    Ok(contenido)
}

Para prototipos o valores que "no pueden fallar", existe unwrap() (pánico si es Err) y expect("mensaje"), que añade contexto al pánico. Úsalos con moderación: en código de producción prefiere ?:

fn main() {
    // Pánico si el archivo no existe, con mensaje claro
    let contenido = fs::read_to_string("hola.txt")
        .expect("no se pudo leer hola.txt");
    println!("{contenido}");
}
El pánico en Rust es controlado: se desenrolla la pila (o aborta según configuración) y libera memoria con su destructor, pero Option y Result son la vía normal para errores esperables como archivos que no existen o JSON mal formado.

Traits

Los traits son el equivalente a interfaces: definen comportamiento que un tipo debe implementar. Un trait se declara con trait y se implementa para un tipo con impl Trait for Tipo:

trait Sonido {
    fn sonido(&self) -> String;
}

struct Perro;
struct Gato;

impl Sonido for Perro {
    fn sonido(&self) -> String {
        String::from("guau")
    }
}

impl Sonido for Gato {
    fn sonido(&self) -> String {
        String::from("miau")
    }
}

// Función genérica que acepta cualquier tipo que implemente Sonido
fn imprimir_sonido(animal: &impl Sonido) {
    println!("{}", animal.sonido());
}

fn main() {
    imprimir_sonido(&Perro); // guau
    imprimir_sonido(&Gato);  // miau
}

Muchos traits se pueden derivar automáticamente con #[derive(...)]. Los más útiles: Debug (impresión de depuración) y Clone (copia explícita):

#[derive(Debug, Clone)]
struct Producto {
    nombre: String,
    precio: f64,
}

fn main() {
    let p1 = Producto {
        nombre: String::from("Teclado"),
        precio: 89.99,
    };
    let p2 = p1.clone(); // copia explícita
    println!("{:?}", p1); // salida de depuración
    println!("{:?}", p2);
}

Proyecto CLI

Construyamos una herramienta de línea de comandos que reciba argumentos.env::args() devuelve el programa y sus argumentos:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let comando = args.get(1).map(|s| s.as_str()).unwrap_or("help");

    match comando {
        "hola" => {
            let nombre = args.get(2).map(|s| s.as_str()).unwrap_or("mundo");
            println!("Hola, {nombre}!");
        }
        "suma" => {
            let a: i32 = args.get(2).map(|s| s.parse().unwrap_or(0)).unwrap_or(0);
            let b: i32 = args.get(3).map(|s| s.parse().unwrap_or(0)).unwrap_or(0);
            println!("{a} + {b} = {}", a + b);
        }
        _ => println!("Comandos: hola [nombre] | suma a b"),
    }
}

Compila con cargo build y ejecuta con cargo run, pasando argumentos después de --:

# Compilar en modo debug
cargo build

# Compilar optimizado (modo release)
cargo build --release

# Ejecutar pasando argumentos
cargo run -- hola Ana
# Salida: Hola, Ana!

cargo run -- suma 4 7
# Salida: 4 + 7 = 11

Cargo también gestiona tests con cargo test y dependencias declaradas en Cargo.toml:

[dependencies]
serde = { version = "1", features = ["derive"] }
clap = "4"

Para CLIs reales, la crate clap es el estándar de facto: parsea argumentos, genera la ayuda y valida tipos automáticamente. Para serializar JSON, serde es imprescindible.


Ejercicios


Con ownership, borrowing, enums, traits y Result ya tienes el núcleo de Rust. El compilador será tu mejor maestro durante las primeras semanas: cada error te enseña algo. Los siguientes pasos naturales son la crate clap para CLIs, serde para serialización, tokio para async y actix-web o axum para servidores web. Rust brilla donde el rendimiento y la fiabilidad no son negociables: sistemas de red, bases de datos, compiladores y WebAssembly.