Un Programa de Ejemplo que Utiliza Estructuras

Beginner

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

Introducción

Bienvenido a Un Programa de Ejemplo que Utiliza Estructuras. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, escribiremos un programa que utiliza estructuras para calcular el área de un rectángulo, refactorizando el código inicial que utilizaba variables separadas para el ancho y la altura.

Un Programa de Ejemplo que Utiliza Estructuras

Para entender cuándo podríamos querer utilizar estructuras, escribamos un programa que calcule el área de un rectángulo. Empezaremos utilizando variables individuales y luego refactorizaremos el programa hasta que lo estemos haciendo con estructuras en lugar de eso.

Vamos a crear un nuevo proyecto binario con Cargo llamado rectángulos que tomará el ancho y el alto de un rectángulo especificado en píxeles y calculará el área del rectángulo. La Lista 5-8 muestra un programa corto con una forma de hacer exactamente eso en el src/main.rs de nuestro proyecto.

Nombre del archivo: src/main.rs

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "El área del rectángulo es {} píxeles cuadrados.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Lista 5-8: Calculando el área de un rectángulo especificado por variables separadas de ancho y alto

Ahora, ejecuta este programa usando cargo run:

El área del rectángulo es 1500 píxeles cuadrados.

Este código logra calcular el área del rectángulo llamando a la función area con cada dimensión, pero podemos hacer más para que este código sea claro y legible.

El problema con este código es evidente en la firma de area:

fn area(width: u32, height: u32) -> u32 {

La función area está supuesta para calcular el área de un rectángulo, pero la función que escribimos tiene dos parámetros y no está claro en ningún lugar de nuestro programa que los parámetros estén relacionados. Sería más legible y más manejable agrupar el ancho y el alto juntos. Ya hemos discutido una forma en que podríamos hacerlo en "El Tipo Tupla": usando tuplas.

Refactorización con Tuplas

La Lista 5-9 muestra otra versión de nuestro programa que utiliza tuplas.

Nombre del archivo: src/main.rs

fn main() {
    let rect1 = (30, 50);

    println!(
        "El área del rectángulo es {} píxeles cuadrados.",
      1 area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
  2 dimensions.0 * dimensions.1
}

Lista 5-9: Especificando el ancho y el alto del rectángulo con una tupla

De un modo, este programa es mejor. Las tuplas nos permiten agregar un poco de estructura y ahora estamos pasando solo un argumento [1]. Pero de otro modo, esta versión es menos clara: las tuplas no nombran sus elementos, por lo que tenemos que acceder por índice a las partes de la tupla [2], lo que hace que nuestro cálculo sea menos obvio.

Intercambiar el ancho y el alto no importaría para el cálculo del área, pero si queremos dibujar el rectángulo en la pantalla, sí importaría! Tendríamos que recordar que width es el índice de la tupla 0 y height es el índice de la tupla 1. Esto sería aún más difícil de entender y recordar para alguien más si tuviera que usar nuestro código. Debido a que no hemos transmitido el significado de nuestros datos en nuestro código, ahora es más fácil introducir errores.

Refactorización con Estructuras: Agregando Más Significado

Usamos estructuras para agregar significado al etiquetar los datos. Podemos transformar la tupla que estamos usando en una estructura con un nombre para todo y nombres para las partes, como se muestra en la Lista 5-10.

Nombre del archivo: src/main.rs

1 struct Rectangle {
  2 width: u32,
    height: u32,
}

fn main() {
  3 let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "El área del rectángulo es {} píxeles cuadrados.",
        area(&rect1)
    );
}

4 fn area(rectangle: &Rectangle) -> u32 {
  5 rectangle.width * rectangle.height
}

Lista 5-10: Definiendo una estructura Rectangle

Aquí, hemos definido una estructura y la hemos nombrado Rectangle [1]. Dentro de las llaves, definimos los campos como width y height, ambos de tipo u32 [2]. Luego, en main, creamos una instancia particular de Rectangle que tiene un ancho de 30 y un alto de 50 [3].

Nuestra función area ahora está definida con un parámetro, que hemos nombrado rectangle, cuyo tipo es un préstamo inmutable de una instancia de la estructura Rectangle [4]. Como se mencionó en el Capítulo 4, queremos prestar la estructura en lugar de tomar posesión de ella. De esta manera, main conserva su posesión y puede continuar usando rect1, que es la razón por la que usamos el & en la firma de la función y donde llamamos a la función.

La función area accede a los campos width y height de la instancia de Rectangle [5] (tenga en cuenta que acceder a los campos de una instancia de estructura prestada no mueve los valores de los campos, que es por lo que a menudo se ven préstamos de estructuras). Nuestra firma de función para area ahora dice exactamente lo que queremos decir: calcular el área de Rectangle, usando sus campos width y height. Esto transmite que el ancho y el alto están relacionados entre sí, y le da nombres descriptivos a los valores en lugar de usar los valores de índice de tupla de 0 y 1. Esto es una victoria para la claridad.

Agregando Funcionalidad Útil con Rasgos Derivados

Sería útil poder imprimir una instancia de Rectangle mientras estamos depurando nuestro programa y ver los valores de todos sus campos. La Lista 5-11 intenta usar la macro println! como lo hemos hecho en capítulos anteriores. Sin embargo, esto no funcionará.

Nombre del archivo: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 es {}", rect1);
}

Lista 5-11: Intentando imprimir una instancia de Rectangle

Cuando compilamos este código, obtenemos un error con este mensaje principal:

error[E0277]: `Rectangle` no implementa `std::fmt::Display`

La macro println! puede hacer muchos tipos de formateo, y por defecto, las llaves curvas le dicen a println! que use un formateo conocido como Display: salida destinada para el consumo directo del usuario final. Los tipos primitivos que hemos visto hasta ahora implementan Display por defecto porque solo hay una forma en que querríamos mostrar un 1 u otro tipo primitivo a un usuario. Pero con las estructuras, la forma en que println! debería formatear la salida es menos clara porque hay más posibilidades de visualización: ¿Quieres comas o no? ¿Quieres imprimir las llaves curvas? ¿Deben mostrarse todos los campos? Debido a esta ambigüedad, Rust no intenta adivinar lo que queremos, y las estructuras no tienen una implementación proporcionada de Display para usar con println! y el marcador de posición {}.

Si seguimos leyendo los errores, encontraremos esta nota útil:

= ayuda: el rasgo `std::fmt::Display` no está implementado para `Rectangle`
= nota: en las cadenas de formato, es posible que puedas usar `{:?}` (o {:#?} para
formato bonito) en su lugar

¡Intentémoslo! La llamada a la macro println! ahora se verá como println!("rect1 es {:?}", rect1);. Colocar el especificador :? dentro de las llaves curvas le dice a println! que queremos usar un formato de salida llamado Debug. El rasgo Debug nos permite imprimir nuestra estructura de una manera que sea útil para los desarrolladores para que podamos ver su valor mientras estamos depurando nuestro código.

Compile el código con este cambio. ¡Ay! Todavía obtenemos un error:

error[E0277]: `Rectangle` no implementa `Debug`

Pero una vez más, el compilador nos da una nota útil:

= ayuda: el rasgo `Debug` no está implementado para `Rectangle`
= nota: agregue `#[derive(Debug)]` o implemente manualmente `Debug`

Rust incluye la funcionalidad para imprimir información de depuración, pero tenemos que optar explícitamente para que esa funcionalidad esté disponible para nuestra estructura. Para hacer eso, agregamos el atributo externo #[derive(Debug)] justo antes de la definición de la estructura, como se muestra en la Lista 5-12.

Nombre del archivo: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 es {:?}", rect1);
}

Lista 5-12: Agregando el atributo para derivar el rasgo Debug e imprimiendo la instancia de Rectangle usando el formateo de depuración

Ahora, cuando ejecutamos el programa, no obtendremos ningún error y veremos la siguiente salida:

rect1 es Rectangle { width: 30, height: 50 }

¡Genial! No es la salida más bonita, pero muestra los valores de todos los campos para esta instancia, lo que definitivamente ayudaría durante la depuración. Cuando tenemos estructuras más grandes, es útil tener una salida un poco más fácil de leer; en esos casos, podemos usar {:#?} en lugar de {:?} en la cadena de println!. En este ejemplo, usar el estilo {:#?} producirá la siguiente salida:

rect1 es Rectangle {
    width: 30,
    height: 50,
}

Otra forma de imprimir un valor usando el formato Debug es usar la macro dbg!, que toma posesión de una expresión (al contrario de println!, que toma una referencia), imprime el archivo y el número de línea donde se produce la llamada a la macro dbg! en su código junto con el valor resultante de esa expresión y devuelve la posesión del valor.

Nota: Llamar a la macro dbg! imprime en el flujo de consola de error estándar (stderr), al contrario de println!, que imprime en el flujo de consola de salida estándar (stdout). Hablaremos más sobre stderr y stdout en "Escribiendo Mensajes de Error en el Error Estándar en lugar de la Salida Estándar".

Aquí hay un ejemplo donde estamos interesados en el valor que se asigna al campo width, así como en el valor de la estructura completa en rect1:

Nombre del archivo: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
      1 width: dbg!(30 * scale),
        height: 50,
    };

  2 dbg!(&rect1);
}

Podemos poner dbg! alrededor de la expresión 30 * scale [1] y, debido a que dbg! devuelve la posesión del valor de la expresión, el campo width tendrá el mismo valor que si no tuviéramos la llamada a dbg! allí. No queremos que dbg! tome posesión de rect1, por lo que usamos una referencia a rect1 en la siguiente llamada [2]. Aquí está cómo se ve la salida de este ejemplo:

[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Podemos ver que la primera parte de la salida proviene de [1] donde estamos depurando la expresión 30 * scale y su valor resultante es 60 (el formateo Debug implementado para enteros es imprimir solo su valor). La llamada a dbg! en [2] imprime el valor de &rect1, que es la estructura Rectangle. Esta salida utiliza el formateo Debug bonito del tipo Rectangle. La macro dbg! puede ser muy útil cuando intentas entender lo que está haciendo tu código.

Además del rasgo Debug, Rust ha proporcionado una serie de rasgos para que los usemos con el atributo derive que pueden agregar un comportamiento útil a nuestros tipos personalizados. Esos rasgos y sus comportamientos se enumeran en el Apéndice C. Cubriremos cómo implementar estos rasgos con un comportamiento personalizado, así como cómo crear tus propios rasgos en el Capítulo 10. También hay muchos atributos diferentes a derive; para obtener más información, consulte la sección "Atributos" de la Referencia de Rust en https://doc.rust-lang.org/reference/attributes.html.

Nuestra función area es muy específica: solo calcula el área de rectángulos. Sería útil vincular este comportamiento más estrechamente a nuestra estructura Rectangle porque no funcionará con ningún otro tipo. Veamos cómo podemos continuar refactorizando este código convirtiendo la función area en un método area definido en nuestro tipo Rectangle.

Resumen

¡Felicitaciones! Has completado el laboratorio An Example Program Using Structs. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.