Definiendo e Instanciando Structs

Los Structs son similares a las tuplas, discutido en la sección “The Tuple Type” en ambos casos mantenemos múltiples valores relativos. Como en las tuplas, las partes de un struct pueden ser de diferentes tipos. A diferencia de las tuplas, en un struct tú nombras a cada pieza de datos para que quede claro, que significan estos valores. Agregando estos nombres significa que los structs son más flexibles que las tuplas: no tienes que confiar en el orden de los datos para especificar o acceder a los valores de una instancia.

Para definir un struct, debemos usar la palabra clave struct y el nombre del struct completo. El nombre del struct debe describir el significado de los datos que se agrupan. Entonces, entre llaves, definimos los nombres y tipos de datos, que llamaremos campos. Por ejemplo, en el Listing 5-1 mostramos una definición de un struct que almacena información sobre una cuenta de usuario.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}

Listing 5-1: Una definición de struct User

Para usar un struct después de haberlo definido, creamos una instancia de ese struct especificando valores concretos para cada uno de los campos. Creamos una instancia al declarar el nombre del struct y luego agregar llaves que contienen clave: valor pares, donde las claves son los nombres de los campos y los valores son los datos que queremos almacenar en esos campos. No tenemos que especificar los campos en el mismo orden en el que los declaramos en el struct. En otras palabras, la definición del struct es como una plantilla general para el tipo, y las instancias llenan esa plantilla con datos particulares para crear valores del tipo. Por ejemplo, podemos declarar un usuario en particular como se muestra en el Listing 5-2.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
    };
}

Listing 5-2: Creando una instancia del struct User

Para acceder a un valor específico de un struct, usamos la notación de punto. Por ejemplo, para acceder a la dirección de correo electrónico de este usuario, usamos user1.email. Si la instancia es mutable, podemos cambiar un valor asignando en un campo particular. El Listing 5-3 muestra cómo cambiar el valor en el campo email de una instancia mutable de User.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
    };

    user1.email = String::from("[email protected]");
}

Listing 5-3: Cambiando el valor en el campo email de una instancia User

Nota que toda la instancia debe ser mutable; Rust no nos permite marcar solo ciertos campos como mutables. Como cualquier expresión, podemos construir una nueva instancia del struct como la última expresión en el cuerpo de la función para devolver implícitamente esa nueva instancia.

Listing 5-4 muestra una función build_user que devuelve una instancia de User con el correo electrónico y el nombre de usuario dados. El campo active obtiene el valor de true, y el campo sign_in_count obtiene el valor de 1.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("[email protected]"),
        String::from("someusername123"),
    );
}

Listing 5-4: Una función build_user que toma un email y username y devuelve una instancia User

Tiene sentido nombrar los parámetros de la función con el mismo nombre que los campos del struct, pero tener que repetir los nombres de los campos y variables email y username es un poco tedioso. Si el struct tuviera más campos, repetir cada nombre sería aún más molesto. Afortunadamente, hay una conveniente forma abreviada.

Usando la abreviatura Field Init

Debido a que los nombres de los parámetros y los nombres de los campos del struct son exactamente los mismos en el Listing 5-4, podemos usar la abreviatura Field Init para reescribir build_user para que se comporte exactamente igual pero no tenga la repetición de username y email, como se muestra en el Listing 5-5.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("[email protected]"),
        String::from("someusername123"),
    );
}

Listing 5-5: Una función build_user que usa field init abreviado porque los parámetros username e email tienen el mismo nombre que los campos del struct

Aquí, estamos creando una nueva instancia del struct User, que tiene un campo llamado email. Queremos establecer el valor del campo email en el valor del parámetro email de la función build_user. Debido a que el campo email y el parámetro email tienen el mismo nombre, solo necesitamos escribir email en lugar de email: email.

Creando Instancias de Otras Instancias con Sintaxis de Struct Update

Suele ser útil crear una nueva instancia de un struct que incluya la mayoría de los valores de otra instancia, pero cambie algunos. Puede hacer esto usando la sintaxis de struct update.

Primero, en el Listing 5-6 mostramos cómo crear una nueva instancia de User regularmente, sin la sintaxis de actualización. Establecemos un nuevo valor para email, pero de lo contrario usamos los mismos valores de user1 que creamos en el Listing 5-2.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("[email protected]"),
        sign_in_count: user1.sign_in_count,
    };
}

Listing 5-6: Creando una nueva instancia User usando uno de los valores de user1

Usando la sintaxis de struct update, podemos lograr el mismo efecto con menos código, como se muestra en el Listing 5-7. La sintaxis .. especifica que los campos restantes que no se establecen explícitamente deben tener el mismo valor que los campos en la instancia dada.

Filename: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("[email protected]"),
        ..user1
    };
}

Listing 5-7: Usando una sintaxis de struct update para introducir un nuevo valor email para una instancia User pero para usar el resto de los valores de user1

El código en el Listing 5-7 también crea una instancia en user2 que tiene un valor diferente para email pero tiene los mismos valores para los campos username, active y sign_in_count de user1. Él ..user1 debe ir al final para especificar que cualquier campo restante debe obtener sus valores del campo correspondiente en user1, pero podemos elegir especificar valores para tantos campos como queramos en cualquier orden, independientemente del orden de los campos en la definición del struct.

Nota que la sintaxis de update struct usa = como una asignación; esto es porque mueve los datos, como vimos en la sección “Variables y datos interactuando con Move”. En este ejemplo, ya no podemos usar user1 como un todo después de crear user2 porque el String en el campo username de user1 se movió a user2. Si hubiéramos dado a user2 nuevos valores String para email y username, y por lo tanto solo usamos los valores de active y sign_in_count de user1, entonces user1 todavía sería válido después de crear user2. Tanto active como sign_in_count son tipos que implementan la trait Copy, por lo que el comportamiento que discutimos en la sección “Datos de pila: Copy” se aplicaría.

Usando Structs de Tuplas sin Campos Nombrados para Crear Diferentes Tipos

Rust también admite structs que se parecen a tuplas, llamados structs de tuplas. Los structs de tuplas tienen el significado adicional que proporciona el nombre del struct, pero no tienen nombres asociados a sus campos; en su lugar, solo tienen los tipos de los campos. Los structs de tuplas son útiles cuando desea darle un nombre al conjunto completo y hacer que el conjunto sea un tipo diferente de otros conjuntos, y cuando nombrar cada campo como en un struct regular sería verboso o redundante.

Para definir un struct de tupla, comience con la palabra clave struct y el nombre del struct seguido por los tipos en la tupla. Por ejemplo, aquí definimos y usamos dos structs de tupla llamados Color y Point:

Filename: src/main.rs

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Nota que los valores black y origin son diferentes tipos porque son instancias de diferentes structs de tupla. Cada struct que defina es su propio tipo, incluso si los campos dentro del struct tienen los mismos tipos. Por ejemplo, una función que toma un parámetro de tipo Color no puede tomar un Point como argumento, incluso si ambos tipos están compuestos por tres valores i32. De lo contrario, las instancias de structs de tupla son similares a las tuplas en que puede descomponerlas en sus piezas individuales, y puede usar un . seguido por el índice para acceder a un valor individual.

Structs de Unidad sin Campos

También puede definir structs que no tienen ningún campo. Estos se llaman structs de unidad porque se comportan de manera similar a (), el tipo de unidad que mencionamos en la sección “El tipo de tupla”. Los structs de unidad pueden ser útiles cuando necesita implementar un trait en algún tipo, pero no tiene datos que desea almacenar en el tipo propio. Discutiremos los traits en el Capítulo 10. Aquí hay un ejemplo de declarar e instanciar un struct de unidad llamado AlwaysEqual:

Filename: src/main.rs

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Para definir AlwaysEqual, usamos la palabra clave struct, el nombre que queremos y luego un punto y coma. ¡No se necesitan llaves ni paréntesis! Luego podemos obtener una instancia de AlwaysEqual en la variable subject de la misma manera: usando el nombre que definimos, sin llaves ni paréntesis. Imagina que más tarde implementaremos un comportamiento para este tipo de tal manera que cada instancia de AlwaysEqual siempre sea igual a cada instancia de cualquier otro tipo, tal vez para tener un resultado conocido para fines de prueba. No necesitaríamos ningún dato para implementar ese comportamiento. Verás en el Capítulo 10 cómo definir traits e implementarlos en cualquier tipo, incluidos los structs de unidad.

Ownership de los datos de Struct

En el struct User de la definición en el Listing 5-1, usamos el tipo String en lugar del tipo &str de la cadena de caracteres. Esta es una elección deliberada porque queremos que cada instancia de este struct tenga todos sus datos y que esos datos sean válidos durante todo el tiempo que el struct sea válido.

También es posible para los structs almacenar referencias a datos que son propiedad de algo más, pero para hacerlo requiere el uso de lifetimes, una característica de Rust que discutiremos en el Capítulo 10. Los lifetimes garantizan que los datos referenciados por un struct sean válidos durante el tiempo que el struct sea válido. Digamos que intentas almacenar una referencia en un struct sin especificar lifetimes, como el siguiente; esto no funcionará:

Filename: src/main.rs

struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: "someusername123",
        email: "[email protected]",
        sign_in_count: 1,
    };
}

El compilador se quejará de que necesita especificadores de lifetime:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors

En el Capítulo 10, discutiremos como solucionar estos errores para que puedas almacenar referencias en structs, pero por ahora, solucionaremos los errores usando tipos propios como String en lugar de referencias como &str.