cv.cm

Programación · Lenguajes de programación

Tutorial de Rust para principiantes

7 min de lectura

Respuesta corta

Rust es un lenguaje de sistemas compilado y de tipado estático, tan rápido como C y C++, pero que evita los errores de memoria en tiempo de compilación, sin recolector de basura. Instálalo con rustup y ejecuta cargo new hello y cargo run. Las ideas centrales son el ownership (propiedad) y el borrowing (préstamo), comprobados por el borrow checker, los structs y enums con match, colecciones como Vec y HashMap, y el manejo de errores con Result y el operador ?.

Abrir Convertidor binario

Qué es Rust y para qué se usa

Rust empezó como un proyecto personal de Graydon Hoare, fue patrocinado por Mozilla y llegó a la versión 1.0 en 2015. Hoy lo gestiona la Rust Foundation. Se usa donde importan a la vez la velocidad y la fiabilidad: herramientas de línea de comandos (ripgrep), servicios web, componentes del navegador en Firefox, partes del kernel de Linux y de Windows, dispositivos embebidos y WebAssembly. Su compilador es estricto, pero a cambio clases enteras de errores, como el use-after-free y las condiciones de carrera, no pueden darse en código Rust seguro.

Instalar Rust y ejecutar Hello, world

Instala la cadena de herramientas con rustup desde rustup.rs. Incluye rustc (el compilador) y Cargo (la herramienta de compilación y gestor de paquetes). Crea un proyecto:

cargo new hello
cd hello
cargo run
# Hello, world!

Cargo genera src/main.rs:

fn main() {
    println!("Hello, world!");
}

println! termina en ! porque es una macro, no una función. Las macros generan código en tiempo de compilación; aquí, la cadena de formato se comprueba al compilar.

Variables y tipos básicos

Las variables son inmutables por defecto; añade mut para permitir cambios. Los tipos suelen inferirse, pero cada valor tiene exactamente un tipo estático, y las conversiones numéricas son explícitas con as.

TipoEjemploNotas
i32, i64, u8, usizelet n: i32 = 42;Con signo (i) y sin signo (u); usize para índices
f64let x = 3.14;Tipo de coma flotante por defecto
boollet ok = true;true o false
charlet c = 'é';Un valor escalar Unicode, 4 bytes
&strlet s = "hi";Slice de cadena prestado
StringString::from("hi")Texto UTF-8 propio y ampliable
Tupla, array(1, "a"), [0; 3]Tamaño fijo
fn main() {
    let name = "Ada";
    let mut count = 7;
    count += 1;
    let avg = count as f64 / 2.0;
    println!("{name} has {count} items, avg {avg}"); // Ada has 8 items, avg 4
    let parsed: i32 = "12".parse().unwrap();
    println!("{}", parsed + 1); // 13
}

Control de flujo y funciones

if, match y los bloques son expresiones: producen valores. La última expresión de una función, sin punto y coma, es su valor de retorno.

fn square(x: i32) -> i32 {
    x * x
}

fn main() {
    let score = 72;
    let grade = if score >= 90 { "A" } else if score >= 70 { "B" } else { "C" };
    println!("{grade}"); // B

    for i in 0..3 {
        print!("{} ", square(i)); // 0 1 4
    }
    println!();

    let mut n = 10;
    while n > 1 {
        n /= 2;
    }
    let label = loop {
        break "done"; // loop can return a value with break
    };
    println!("{n} {label}"); // 1 done
}

Característica distintiva: ownership y borrowing

Cada valor tiene exactamente un propietario. Cuando el propietario sale del ámbito, el valor se descarta (se libera). Asignar un valor del heap, como un String, a otra variable lo mueve, y el nombre antiguo ya no se puede usar. Para usar un valor sin tomar su propiedad, tómalo prestado con una referencia: en cada momento puede haber cualquier número de referencias compartidas &T, o exactamente una referencia mutable &mut T.

fn length(s: &str) -> usize {
    s.len() // borrows, does not take ownership
}

fn add_exclamation(s: &mut String) {
    s.push('!');
}

fn main() {
    let a = String::from("hello");
    let b = a; // ownership moves to b
    // println!("{a}"); // error: borrow of moved value: `a`
    let mut c = b.clone(); // explicit deep copy
    add_exclamation(&mut c);
    println!("{} {} {}", b, c, length(&c)); // hello hello! 6
}

El borrow checker aplica estas reglas en tiempo de compilación, y así consigue Rust la seguridad de memoria sin recolector de basura. Los tipos simples, como los enteros, implementan Copy, así que se copian en lugar de moverse.

Structs, enums y pattern matching

Los structs agrupan campos y los bloques impl añaden métodos. Los enums pueden llevar datos en cada variante, y match debe cubrir todas las variantes, algo que comprueba el compilador. El enum estándar Option<T> (Some(value) o None) sustituye a null.

#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn distance(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rect { w, h } => w * h,
    }
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("{:?} {}", p, p.distance()); // Point { x: 3.0, y: 4.0 } 5
    println!("{}", area(&Shape::Rect { w: 2.0, h: 3.0 })); // 6
    let nums = [10, 20];
    if let Some(v) = nums.first() {
        println!("first is {v}"); // first is 10
    }
}

Los traits definen comportamiento compartido, de forma parecida a las interfaces: #[derive(Debug)] pide al compilador que implemente el trait Debug para que {:?} pueda imprimir el struct.

Colecciones: Vec y HashMap

use std::collections::HashMap;

fn main() {
    let mut nums = vec![3, 1, 2];
    nums.push(5);
    nums.sort();
    println!("{:?} {}", nums, nums.len()); // [1, 2, 3, 5] 4

    let evens: Vec<i32> = nums.iter().filter(|n| *n % 2 == 0).map(|n| n * n).collect();
    println!("{:?}", evens); // [4]

    let mut ages: HashMap<&str, u32> = HashMap::new();
    ages.insert("ann", 31);
    ages.insert("bob", 25);
    *ages.entry("ann").or_insert(0) += 1;
    println!("{:?} {:?}", ages.get("ann"), ages.get("zed")); // Some(32) None
}

Los iteradores con closures (|n| ...), como filter, map y collect, compilan a código tan rápido como un bucle escrito a mano.

Manejo de errores con Result y ?

Las funciones que pueden fallar devuelven Result<T, E>: o bien Ok(value) o bien Err(error). El operador ? devuelve el error a quien llama de forma anticipada, así que los caminos de error se mantienen cortos. Los errores irrecuperables usan panic!.

use std::num::ParseIntError;

fn parse_age(text: &str) -> Result<u32, ParseIntError> {
    let age: u32 = text.trim().parse()?; // returns Err early on bad input
    Ok(age)
}

fn main() {
    match parse_age("41") {
        Ok(age) => println!("age {age}"), // age 41
        Err(e) => println!("error: {e}"),
    }
    match parse_age("abc") {
        Ok(age) => println!("age {age}"),
        Err(e) => println!("error: {e}"), // error: invalid digit found in string
    }
}

Un programa completo pequeño: frecuencia de palabras

Ponlo en src/main.rs y ejecuta cargo run -- notes.txt.

use std::collections::HashMap;
use std::{env, fs, process};

fn main() {
    let path = env::args().nth(1).unwrap_or_else(|| {
        eprintln!("usage: words FILE");
        process::exit(1);
    });
    let text = match fs::read_to_string(&path) {
        Ok(t) => t.to_lowercase(),
        Err(e) => {
            eprintln!("cannot read {path}: {e}");
            process::exit(1);
        }
    };

    let mut counts: HashMap<&str, usize> = HashMap::new();
    for word in text.split(|c: char| !c.is_alphanumeric()).filter(|w| !w.is_empty()) {
        *counts.entry(word).or_insert(0) += 1;
    }

    let mut top: Vec<(&str, usize)> = counts.into_iter().collect();
    top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
    for (word, n) in top.iter().take(5) {
        println!("{n:5}  {word}");
    }
}

El HashMap almacena slices &str que toman prestado de text, así que no se copia ninguna palabra; el borrow checker garantiza que text viva el tiempo suficiente.

Errores comunes de principiantes

  • Usar un valor después de haberlo movido. Tómalo prestado (&value) o clónalo a propósito.
  • Intentar indexar un String con s[0]. Las cadenas son UTF-8, así que usa s.chars() o slices de bytes en los límites de carácter.
  • Mantener una referencia a un Vec mientras se le añaden elementos; el push puede reasignar la memoria, por lo que el compilador lo prohíbe.
  • Confundir String (propio) y &str (prestado). Acepta &str en los parámetros de las funciones para admitir ambos.

Adónde ir después

Lee «The Rust Programming Language» (conocido como el libro), disponible gratis en doc.rust-lang.org, y prueba Rust by Example para ejemplos cortos ejecutables. Después aprende a fondo los traits y los genéricos, los lifetimes, las pruebas con cargo test y crates de crates.io como serde para serialización y tokio para programas asíncronos. Ejecuta cargo clippy para recibir más consejos de lint.

Preguntas frecuentes

¿Es difícil aprender Rust?

Las primeras semanas son más duras que en la mayoría de los lenguajes, porque el ownership y el borrowing son ideas nuevas. Cuando te resultan familiares, el compilador detecta muchos errores pronto, y a muchos desarrolladores el código Rust les resulta más fácil de mantener.

¿Rust tiene recolector de basura?

No. La memoria se libera automáticamente cuando su propietario sale del ámbito, algo que se decide en tiempo de compilación según las reglas de ownership. Existen los tipos con conteo de referencias Rc y Arc para la propiedad compartida.

¿Qué es el Rust unsafe?

El código dentro de un bloque unsafe puede hacer cosas que el compilador no puede verificar, como desreferenciar punteros en bruto o llamar a funciones de C. Se usa con moderación, normalmente envuelto en una API segura.

Rust o Go: ¿cuál debo aprender?

Go es más sencillo y rápido de aprender y es popular para servicios de red. Rust da más control y rendimiento con garantías más fuertes, a costa de una curva de aprendizaje más empinada.

Todos los tutoriales de programación