Aprende Rust

Lección 13 de 26 Colecciones y errores

Vectores: Vec<T>

  • 4 min de lectura
  • 4 ejercicios
  1. Un array tiene tamaño fijo. Un vector Vec<T> crece y encoge: ideal cuando no sabes cuántos datos tendrás.
  2. push añade un elemento al final. Por eso pila es mut: el vector va a cambiar.
  3. pop quita el último y devuelve un Option: Some(3), o None si el vector está vacío.
  4. Los índices empiezan en 0. pila[5] hace panic porque no existe; pila.get(5) devuelve None sin fallar.
1/4

Los arrays tienen un tamaño fijo que se decide al compilar: un [i32; 3] siempre tendrá tres elementos. Pero casi nunca sabes de antemano cuántos datos vas a manejar: los productos de un carrito, los mensajes de un chat, las líneas de un fichero…

Para eso está Vec<T>, el vector: una lista de elementos del mismo tipo que puede crecer y encoger mientras el programa se ejecuta. Sus elementos se guardan en el heap, uno detrás de otro. Es, con diferencia, la colección más usada de Rust.

Crear un vector

Hay dos formas: Vec::new() crea un vector vacío y la macro vec! lo crea con valores iniciales.

fn main() {
    let vacio: Vec<i32> = Vec::new();
    let letras = vec!['a', 'b', 'c'];

    println!("{vacio:?} {letras:?}");
    println!("Letras: {}", letras.len());
    println!("¿Vacío? {}", vacio.is_empty());
}

Con Vec::new() indicamos el tipo (Vec<i32>) porque el vector está vacío y Rust no tiene de dónde deducirlo. Con vec! no hace falta: lo saca de los valores. len() devuelve cuántos elementos hay y is_empty() si no hay ninguno.

Añadir y quitar: push y pop

push añade un elemento al final y pop quita el último. Como el vector tiene que cambiar, debe ser mut:

fn main() {
    let mut pila = Vec::new();
    pila.push(1);
    pila.push(2);
    pila.push(3);

    println!("{:?}", pila.pop()); // Some(3)
    println!("{pila:?}");         // [1, 2]

    let mut vacia: Vec<i32> = Vec::new();
    println!("{:?}", vacia.pop()); // None
}

Fíjate en que aquí no hace falta indicar el tipo de pila: Rust lo deduce del primer push. Y pop devuelve un Option: un vector vacío no tiene último elemento que quitar, así que devuelve None en lugar de fallar.

Leer elementos: índice o get

Con v[i] accedes a la posición i (empezando en 0), como en un array. Pero si la posición no existe, el programa hace panic:

fn main() {
    let v = vec![10, 20, 30];
    println!("{}", v[1]);
    println!("{}", v[10]);
}

Si la posición puede no existir, usa get. Devuelve un Option con una referencia al elemento:

fn main() {
    let v = vec![10, 20, 30];

    match v.get(10) {
        Some(x) => println!("Elemento: {x}"),
        None => println!("No hay nada en esa posición"),
    }
}

Regla práctica: Usa v[i] cuando sea un error de programación que la posición no exista y get cuando sea algo normal, por ejemplo si el índice lo escribe el usuario.

Recorrer un vector

Con for x in &v recorres el vector prestado: x es una referencia a cada elemento y el vector sigue siendo tuyo después del bucle. Si quieres modificar los elementos, usa &mut v y el operador * para cambiar el valor al que apunta cada referencia:

fn main() {
    let mut precios = vec![10, 20, 30];

    for p in &precios {
        println!("Precio: {p}");
    }

    for p in &mut precios {
        *p += 5;
    }

    println!("{precios:?}");
}

Si escribes for x in v, sin &, el bucle se queda con el vector (lo mueve) y no podrás usarlo después.

Vectores y préstamos

Las reglas de los préstamos también se aplican aquí. Este código parece inofensivo, pero no compila:

fn main() {
    let mut v = vec![1, 2, 3];
    let primero = &v[0];
    v.push(4);
    println!("{primero}");
}

primero es un préstamo de v y, mientras siga vivo, no puedes modificar v. No es un capricho: si el vector no tiene sitio para el nuevo elemento, push reserva un bloque de memoria más grande, copia ahí los elementos y libera el antiguo. primero seguiría apuntando a la memoria liberada.

En otros lenguajes este error aparece de vez en cuando en ejecución y es difícil de encontrar. En Rust, el compilador lo detecta antes de que exista.

Otros métodos útiles

fn main() {
    let mut v = vec![5, 3, 8];

    v.insert(0, 9);            // [9, 5, 3, 8]
    let quitado = v.remove(2); // quita el 3: [9, 5, 8]
    v.sort();                  // [5, 8, 9]

    println!("{v:?}, he quitado el {quitado}");
    println!("¿Tiene un 8? {}", v.contains(&8));
}
MétodoQué hace
push(x)Añade x al final
pop()Quita el último y lo devuelve en un Option
insert(i, x)Mete x en la posición i y desplaza el resto
remove(i)Quita el elemento de la posición i y lo devuelve
get(i)Devuelve Some(&x) o None
contains(&x)true si el vector contiene x
sort()Ordena el vector de menor a mayor
len() / is_empty()Número de elementos / si está vacío

contains recibe una referencia (&8) porque solo necesita mirar el valor, no quedárselo. Y ten en cuenta que insert y remove tienen que desplazar los elementos que van detrás: en vectores enormes son más lentos que push y pop.

Funciones que reciben vectores

Si una función solo necesita leer el vector, recibe un slice &[T] en lugar de &Vec<T>. Así acepta tanto &Vec<T> como arrays:

fn sumar(numeros: &[i32]) -> i32 {
    let mut total = 0;
    for n in numeros {
        total += n;
    }
    total
}

fn main() {
    let v = vec![1, 2, 3];
    println!("{}", sumar(&v));
    println!("{}", sumar(&[10, 20]));
}

Mezclar tipos con un enum

Un vector solo guarda elementos de un tipo. Si necesitas mezclar, por ejemplo, las celdas de una hoja de cálculo, crea un enum con una variante por cada tipo: todas son del mismo tipo para el vector.

enum Celda {
    Entero(i32),
    Decimal(f64),
    Texto(String),
}

fn main() {
    let fila = vec![
        Celda::Entero(3),
        Celda::Texto(String::from("azul")),
        Celda::Decimal(10.5),
    ];

    for celda in &fila {
        match celda {
            Celda::Entero(n) => println!("Entero: {n}"),
            Celda::Decimal(d) => println!("Decimal: {d}"),
            Celda::Texto(t) => println!("Texto: {t}"),
        }
    }
}

Y como match es exhaustivo, el compilador comprueba que tratas todos los tipos posibles.

Hora de practicar

Ejercicios

0/4 resueltos

Reto 1 de 4

Crea un vector vacío compra con Vec::new(), añade "pan", "leche" y "huevos" con push y después quita el último con pop. Imprime lo que devuelve pop y cómo queda el vector, exactamente como indican los comentarios.
Salida esperada
Quitado: Some("huevos")
Quedan 2: ["pan", "leche"]
src/main.rs
$ cargo run Listo

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

¿Algo no cuadra en este reto? Avísanos