Definición e instanciación de estructuras

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Definiendo e Instanciando Estructuras. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, aprenderemos a definir e instanciar estructuras en Rust, donde las estructuras almacenan múltiples valores relacionados y pueden tener campos con nombres, lo que permite un uso y acceso más flexible de los datos.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100395{{"Definición e instanciación de estructuras"}} rust/mutable_variables -.-> lab-100395{{"Definición e instanciación de estructuras"}} rust/integer_types -.-> lab-100395{{"Definición e instanciación de estructuras"}} rust/boolean_type -.-> lab-100395{{"Definición e instanciación de estructuras"}} rust/string_type -.-> lab-100395{{"Definición e instanciación de estructuras"}} rust/function_syntax -.-> lab-100395{{"Definición e instanciación de estructuras"}} rust/expressions_statements -.-> lab-100395{{"Definición e instanciación de estructuras"}} rust/method_syntax -.-> lab-100395{{"Definición e instanciación de estructuras"}} end

Definiendo e Instanciando Estructuras

Las estructuras son similares a las tuplas, discutidas en "El Tipo Tupla", en el sentido de que ambas almacenan múltiples valores relacionados. Al igual que las tuplas, las partes de una estructura pueden ser de diferentes tipos. A diferencia de las tuplas, en una estructura se le asignará un nombre a cada parte de datos para que quede claro qué significan los valores. Agregar estos nombres significa que las estructuras son más flexibles que las tuplas: no tienes que depender del orden de los datos para especificar o acceder a los valores de una instancia.

Para definir una estructura, ingresamos la palabra clave struct y nombramos toda la estructura. El nombre de una estructura debe describir la importancia de las piezas de datos que se agrupan. Luego, dentro de llaves, definimos los nombres y tipos de las piezas de datos, que llamamos campos. Por ejemplo, la Lista 5-1 muestra una estructura que almacena información sobre una cuenta de usuario.

Nombre del archivo: src/main.rs

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

Lista 5-1: Definición de una estructura User

Para usar una estructura después de haberla definido, creamos una instancia de esa estructura especificando valores concretos para cada uno de los campos. Creamos una instancia indicando el nombre de la estructura y luego agregamos llaves que contienen pares clave: valor, 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 que los declaramos en la estructura. En otras palabras, la definición de la estructura 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 la Lista 5-2.

Nombre del archivo: src/main.rs

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

Lista 5-2: Creación de una instancia de la estructura User

Para obtener un valor específico de una estructura, 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 usando la notación de punto y asignando a un campo particular. La Lista 5-3 muestra cómo cambiar el valor en el campo email de una instancia mutable de User.

Nombre del archivo: src/main.rs

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]");
}

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

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

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

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

Lista 5-4: Una función build_user que toma un correo electrónico y nombre de usuario y devuelve una instancia de User

Tiene sentido nombrar los parámetros de la función con el mismo nombre que los campos de la estructura, pero tener que repetir los nombres de los campos email y username y las variables es un poco tedioso. Si la estructura tuviera más campos, repetir cada nombre sería aún más molesto. Por suerte, hay un atajo práctico.

Usando la Sintaxis Corta de Inicialización de Campos

Debido a que los nombres de los parámetros y los nombres de los campos de la estructura son exactamente los mismos en la Lista 5-4, podemos usar la sintaxis de sintaxis corta de inicialización de campos para volver a escribir build_user de modo que se comporte exactamente igual pero sin la repetición de username y email, como se muestra en la Lista 5-5.

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

Lista 5-5: Una función build_user que utiliza la sintaxis corta de inicialización de campos porque los parámetros username y email tienen el mismo nombre que los campos de la estructura

Aquí, estamos creando una nueva instancia de la estructura 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 a Partir de Otras Instancias con la Sintaxis de Actualización de Estructura

A menudo es útil crear una nueva instancia de una estructura que incluye la mayoría de los valores de otra instancia, pero cambia algunos. Puedes hacer esto usando la sintaxis de actualización de estructura.

Primero, en la Lista 5-6 mostramos cómo crear una nueva instancia de User en user2 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 la Lista 5-2.

Nombre del archivo: src/main.rs

fn main() {
    --snip--

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

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

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

Nombre del archivo: src/main.rs

fn main() {
    --snip--


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

Lista 5-7: Usando la sintaxis de actualización de estructura para establecer un nuevo valor de email para una instancia de User pero para usar el resto de los valores de user1

El código en la Lista 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. El ..user1 debe ir al final para especificar que cualquier campo restante debe obtener sus valores de los campos correspondientes 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 de la estructura.

Tenga en cuenta que la sintaxis de actualización de estructura usa = como una asignación; esto es porque mueve los datos, al igual que vimos en "Variables y Datos Interactuando con Move". En este ejemplo, ya no podemos usar user1 después de crear user2 porque la String en el campo username de user1 se movió a user2. Si hubiéramos dado nuevos valores de String a user2 tanto para email como para username, y por lo tanto solo hubiéramos usado 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 el trato Copy, por lo que se aplicaría el comportamiento que discutimos en "Datos Solo en la Pila: Copy".

Usando Estructuras Tupla Sin Campos con Nombres para Crear Diferentes Tipos

Rust también admite estructuras que se parecen a las tuplas, llamadas estructuras tupla. Las estructuras tupla tienen el significado adicional que proporciona el nombre de la estructura pero no tienen nombres asociados a sus campos; en cambio, solo tienen los tipos de los campos. Las estructuras tupla son útiles cuando quieres darle un nombre a toda la tupla y hacer que la tupla sea de un tipo diferente de otras tuplas, y cuando nombrar cada campo como en una estructura regular sería verboso o redundante.

Para definir una estructura tupla, comienza con la palabra clave struct y el nombre de la estructura seguido de los tipos en la tupla. Por ejemplo, aquí definimos y usamos dos estructuras tupla llamadas Color y Point:

Nombre del archivo: 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);
}

Tenga en cuenta que los valores black y origin son de diferentes tipos porque son instancias de diferentes estructuras tupla. Cada estructura que defines es su propio tipo, aunque los campos dentro de la estructura pueden tener los mismos tipos. Por ejemplo, una función que toma un parámetro de tipo Color no puede tomar un Point como argumento, aunque ambos tipos están compuestos por tres valores de i32. De lo contrario, las instancias de estructuras tupla son similares a las tuplas en el sentido de que se pueden desestructurar en sus piezas individuales, y se puede usar un . seguido del índice para acceder a un valor individual.

Estructuras Similares a la Unidad Sin Ningún Campo

También puedes definir estructuras que no tienen ningún campo. ¡Estas se llaman estructuras similares a la unidad porque se comportan de manera similar a (), el tipo unidad que mencionamos en "El Tipo Tupla". Las estructuras similares a la unidad pueden ser útiles cuando necesitas implementar un trato en algún tipo pero no tienes ningún dato que desees almacenar en el tipo mismo. Discutiremos los tratados en el Capítulo 10. Aquí hay un ejemplo de declarar e instanciar una estructura unidad llamada AlwaysEqual:

Nombre del archivo: 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 es necesario usar llaves ni paréntesis! Luego podemos obtener una instancia de AlwaysEqual en la variable subject de manera similar: usando el nombre que definimos, sin ninguna llave ni paréntesis. Imagina que más adelante implementaremos un comportamiento para este tipo de manera que cada instancia de AlwaysEqual siempre sea igual a cada instancia de cualquier otro tipo, quizás para tener un resultado conocido con fines de prueba. ¡No necesitaríamos ningún dato para implementar ese comportamiento! Verás en el Capítulo 10 cómo definir tratados e implementarlos en cualquier tipo, incluyendo estructuras similares a la unidad.

Propiedad de los Datos de la Estructura

En la definición de la estructura User en la Lista 5-1, usamos el tipo String con propiedad en lugar del tipo de segmento de cadena &str. Esta es una elección deliberada porque queremos que cada instancia de esta estructura sea dueña de todos sus datos y que esos datos sean válidos durante todo el tiempo que la estructura completa sea válida.

También es posible que las estructuras almacenen referencias a datos propiedad de algo más, pero para hacer eso se requiere el uso de vidas de tiempo, una característica de Rust que discutiremos en el Capítulo 10. Los períodos de vida aseguran que los datos referenciados por una estructura sean válidos durante todo el tiempo que la estructura lo sea. Digamos que intentas almacenar una referencia en una estructura sin especificar los períodos de vida, como lo siguiente en src/main.rs; esto no funcionará:

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 períodos de vida:

$ `cargo run`
   Compilando 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,
  |

En el Capítulo 10, discutiremos cómo corregir estos errores para que puedas almacenar referencias en estructuras, pero por ahora, corregiremos errores como estos usando tipos con propiedad como String en lugar de referencias como &str.

Resumen

¡Felicitaciones! Has completado el laboratorio de Definición e Instanciación de Estructuras. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.