rust-structs-definicion-instanciacion

Structs en Rust: crear y usar tipos de datos propios

  • 5 min

Un struct es un tipo propio que agrupa campos relacionados bajo un mismo nombre.

Hasta este punto del curso, hemos estado manipulando átomos: un entero i32, un flotante f64, un booleano bool. La programación trata de modelar la realidad, y en la realidad las cosas son moléculas complejas formadas por muchas partes.

Si vienes de lenguajes orientados a objetos (C#, Java, C++), estarás buscando las class. En Rust, no tenemos clases, tenemos structs (estructuras).

Aunque a primera vista parezcan lo mismo (agrupan datos), los struct de Rust son más parecidos a los de C o a los objetos planos de datos (POCO/DTO) que a las clases de Java. Los datos y sus bloques de implementación se declaran por separado.

Hoy vamos a ver cómo definir la forma de nuestros datos.

Definir un struct clásico

Un struct nos permite nombrar y empaquetar múltiples valores relacionados. A cada pieza de dato la llamamos campo (field).

Para definirlo, usamos la palabra clave struct, le damos un nombre (en PascalCase) y definimos sus campos y tipos entre llaves.

struct Usuario {
    username: String,
    email: String,
    intentos_login: u64,
    activo: bool,
}
Copied!

Nota sobre String vs &str: Fíjate que en el struct hemos usado String, no &str. Esto es intencional. Queremos que el struct sea dueño (Owner) de sus datos. Si usáramos &str (referencias), necesitaríamos usar Lifetimes ('a), un concepto avanzado que veremos más adelante. Por ahora, usa siempre String dentro de los structs.

Crear una instancia

Una vez definido el plano, podemos construir (instanciar) objetos concretos. No usamos la palabra new como en otros lenguajes. Simplemente escribimos el nombre del struct y rellenamos los campos clave-valor.

fn main() {
    // Creamos una instancia inmutable
    let usuario1 = Usuario {
        email: String::from("[email protected]"),
        username: String::from("luisllamas"),
        activo: true,
        intentos_login: 1,
    };

    // Accedemos a los campos con el punto (.)
    println!("El usuario es: {}", usuario1.username);
}
Copied!

El orden en que definimos los campos no importa al instanciarlo.

Mutabilidad en los struct

No puedes marcar solo un campo como mutable. La mutabilidad es una propiedad de la instancia completa, no de sus partes.

  • Si la variable es let, todo el struct es inmutable.
  • Si la variable es let mut, todos los campos son mutables.
fn main() {
    let mut usuario1 = Usuario {
        email: String::from("[email protected]"),
        username: String::from("admin"),
        activo: true,
        intentos_login: 0,
    };

    usuario1.email = String::from("[email protected]"); // ✅ Correcto
    // usuario1.username también es mutable
}
Copied!

Atajos de sintaxis

Rust tiene un par de trucos para escribir menos código al inicializar structs, muy similares a los que existen en JavaScript moderno.

Si tienes variables con el mismo nombre que los campos del struct, no hace falta repetir campo: campo.

fn crear_usuario(email: String, username: String) -> Usuario {
    Usuario {
        email,      // Equivale a email: email
        username,   // Equivale a username: username
        activo: true,
        intentos_login: 0,
    }
}
Copied!

A menudo querrás crear una nueva instancia de un struct basándote en otra, cambiando solo un par de valores. En lugar de copiar campo a campo manualmente, usamos la sintaxis de actualización ...

fn main() {
    let usuario1 = Usuario {
        email: String::from("[email protected]"),
        username: String::from("user1"),
        activo: true,
        intentos_login: 0,
    };

    // Creamos usuario2 copiando TODO de usuario1, excepto el email
    let usuario2 = Usuario {
        email: String::from("[email protected]"),
        ..usuario1 // <--- Reutilizamos los campos restantes
    };
}
Copied!

Cuidado con el Ownership: La sintaxis ..usuario1 mueve los datos. En el ejemplo anterior, el campo username es un String (no implementa Copy). Por tanto, el username de usuario1 se ha movido a usuario2.

Resultado: usuario1 queda parcialmente movido y ya no podemos usarlo como un valor completo. Todavía podríamos acceder a campos que no se hayan movido, como email o los campos Copy, pero no a username.

A veces quieres un tipo de dato nuevo, pero ponerle nombre a cada campo es redundante o excesivo. Para esto, Rust ofrece los Tuple Structs. Son híbridos entre tuplas y structs: tienen nombre de tipo, pero sus campos son anónimos.

struct Color(i32, i32, i32);
struct Punto(i32, i32, i32);

fn main() {
    let negro = Color(0, 0, 0);
    let origen = Punto(0, 0, 0);

    // Se accede por índice, igual que las tuplas
    println!("El componente rojo es: {}", negro.0);
}
Copied!

¿Para qué sirve esto si ya tenemos tuplas normales (i32, i32, i32)? Para dar significado y seguridad de tipos.

Aunque Color y Punto almacenan los mismos datos (tres enteros), para Rust son tipos diferentes. No puedes pasar una variable Color a una función que espera un Punto. Esto evita bugs tontos donde mezclas coordenadas con colores RGB.

Structs de tipo unidad

Finalmente, podemos definir structs que no tienen ningún campo.

struct SiempreIgual;
Copied!

Se comportan como el tipo unidad (). Parecen inútiles ahora, pero son muy importantes cuando trabajemos con Traits. A veces necesitamos un tipo solo para implementar un comportamiento (una interfaz), sin necesidad de guardar datos.