DEV Community

SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 5.2. Struct Usage Example - Printing Debug Information

5.2.1. Example Requirements

Create a function that calculates the area of a rectangle. The width and length are both of type u32, and the area is also of type u32.

5.2.2. The Simple Approach

The simplest solution is to define the function with two parameters: one for the width and one for the length, both of type &u32 (the example says the values are u32, and in this case the function does not need to take ownership of the data, so we use references by adding & in front of the type). Inside the function, just return the product of the width and length.

fn main() {  
    let width = 30;  
    let length = 50;  
    println!("{}", area(&width, &length));  
}  

fn area(width: &u32, length: &u32) -> u32 {  
    width * length  
}
Enter fullscreen mode Exit fullscreen mode

Output:

1500
Enter fullscreen mode Exit fullscreen mode

5.2.3. The Tuple Approach

The simple approach itself is fine, but it has a maintainability problem: width and length are separate parameters, so nowhere in the program is it clear that these parameters are related. Combining the width and height into one value is more readable and easier to manage. For organizing data, a tuple is perfect for this (because the values are the same data type, using an array here would also be fine).

fn main() {  
    let rectangle = (30,50);  
    println!("{}", area(&rectangle));  
}  

fn area(dim:&(u32,u32)) -> u32 {  
    dim.0 * dim.1  
}
Enter fullscreen mode Exit fullscreen mode

Output:

1500
Enter fullscreen mode Exit fullscreen mode

5.2.4. The Struct Approach

The tuple approach does improve maintainability, but the code becomes less readable, because without comments no one knows whether the first item in the tuple represents the width or the length (although that does not matter for calculating area, it matters in larger projects). Tuple elements do not have names. Even tuple structs, which were covered in the previous article, do not have named elements either.

So what kind of data structure can combine two values and give each of them a name? That’s right: struct.

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

fn main() {  
    let rectangle = Rectangle{  
        width: 30,  
        length: 50,  
    };  
    println!("{}", area(&rectangle));  
}  

fn area(dim:&Rectangle) -> u32 {  
    dim.width * dim.length  
}
Enter fullscreen mode Exit fullscreen mode

5.2.5. Printing Debug Information for Structs

Starting from the code above, what happens if we add one more line to print the rectangle instance directly? The code is as follows:

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

fn main() {  
    let rectangle = Rectangle{  
        width: 30,  
        length: 50,  
    };  
    println!("{}", area(&rectangle));  
    println!("{}", rectangle);  // Print the instance directly
}  

fn area(dim:&Rectangle) -> u32 {  
    dim.width * dim.length  
}
Enter fullscreen mode Exit fullscreen mode

Output:

error[E0277]: `Rectangle` does not implement `std::fmt::Display`
  --> src/main.rs:12:20
   |
12 |     println!("{}", rectangle);
   |                    ^^^^^^^^^ `Rectangle` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the `println` macro (in Nightly builds, run with -Z macro-backtrace for more info)
Enter fullscreen mode Exit fullscreen mode

First, let’s explain the error: the println! macro can perform many kinds of formatted printing. The {} placeholder tells println! to use the std::fmt::Display trait (think of it as an interface), similar to Python’s toString. The error message tells us that Rectangle does not implement the std::fmt::Display trait, so it cannot be printed this way.

In fact, the basic data types we have covered so far all implement std::fmt::Display by default, because their display format is fairly straightforward. For example, if you print 1, the program can only print the Arabic numeral 1. But for Rectangle, which has two fields, should it print both, only width, or only length? There are too many possibilities, so Rust does not implement std::fmt::Display for structs by default.

But if we keep reading the next line:

= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Enter fullscreen mode Exit fullscreen mode

the compiler is telling us that we can use {:?} or {:#?} instead of {}. Let’s try the first one:

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

fn main() {  
    let rectangle = Rectangle{  
        width: 30,  
        length: 50,  
    };  
    println!("{}", area(&rectangle));  
    println!("{:?}", rectangle);  // Change `{}` to `{:?}`
}  

fn area(dim:&Rectangle) -> u32 {  
    dim.width * dim.length  
}
Enter fullscreen mode Exit fullscreen mode

It still fails:

error[E0277]: `Rectangle` does not implement `Debug`
  --> src/main.rs:12:22
   |
12 |     println!("{:?}", rectangle);
   |                      ^^^^^^^^^ `Rectangle` cannot be formatted using `{:?}`
   |
   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the `println` macro (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Rectangle` with `#[derive(Debug)]`
   |
1  + #[derive(Debug)]
2  | struct Rectangle {
   |
Enter fullscreen mode Exit fullscreen mode

But the error message has changed. Last time it said std::fmt::Display was not implemented; this time it says Debug is not implemented. Debug, like Display, is also a formatting method. If we keep reading the note:

= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
Enter fullscreen mode Exit fullscreen mode

the compiler is suggesting that we add #[derive(Debug)] to the code or manually implement the Debug trait. Here we will use the first option (the second one will be covered in the next article):

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

fn main() {  
    let rectangle = Rectangle{  
        width: 30,  
        length: 50,  
    };  
    println!("{}", area(&rectangle));  
    println!("{:?}", rectangle);  
}  

fn area(dim:&Rectangle) -> u32 {  
    dim.width * dim.length  
}
Enter fullscreen mode Exit fullscreen mode

Output:

1500
Rectangle { width: 30, length: 50 }
Enter fullscreen mode Exit fullscreen mode

This time it works. Rust itself includes debug-printing functionality, but you must explicitly opt in for structs in your own code, so you need to add the #[derive(Debug)] attribute before the struct definition. This output shows the struct name, the field names, and their values.

Sometimes a struct has many fields, and the horizontal layout produced by {:?} is not very readable. If you want a more readable output, change {:?} to {:#?}:

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

fn main() {  
    let rectangle = Rectangle{  
        width: 30,  
        length: 50,  
    };  
    println!("{}", area(&rectangle));  
    println!("{:#?}", rectangle);  
}  

fn area(dim:&Rectangle) -> u32 {  
    dim.width * dim.length  
}
Enter fullscreen mode Exit fullscreen mode

Output:

1500
Rectangle {
    width: 30,
    length: 50,
}
Enter fullscreen mode Exit fullscreen mode

In this output, the fields are arranged vertically, which is more readable for structs with many fields.

In fact, Rust provides many traits that we can derive. These traits can add a lot of functionality to custom types. All traits and their behavior can be found in the official guide, and I have attached the link here.

In the code above, Rectangle derives the Debug trait, so it can be printed in debug mode.

Let’s look at another example. Suppose you have a struct representing a point:

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point1 = Point { x: 1, y: 2 };
    let point2 = point1.clone();
    println!("{:?}", point1); // Print Point using the Debug trait
    assert_eq!(point1, point2); // Compare two Point values using the PartialEq trait
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • #[derive(Debug)] allows you to print an instance of the Point struct using the {:?} formatting specifier.
  • #[derive(Clone)] allows you to create a copy of a Point instance.
  • #[derive(PartialEq)] allows you to compare whether two Point instances are equal.

Top comments (0)