Rust, a modern programming language, has gained immense popularity due to its focus on memory safety, performance, and expressive syntax. One of the fundamental features of Rust is its powerful system for defining and using structs, along with effective debugging tools that assist developers in identifying and resolving issues. In this article, we delve into the world of Rust's structs and debugging techniques, exploring their significance and providing practical insights into their usage.
Understanding Structs in Rust
Structs, short for structures, are user-defined data types that allow developers to create custom data structures. These structures enable the grouping of related data fields under a single unit, facilitating the organization and manipulation of data. In Rust, struct definitions consist of field names and their associated data types, forming a blueprint for creating instances of the struct. This approach ensures that data remains organized and encapsulated within the struct, promoting code modularity and readability.
Notably, Rust's struct concept bears resemblance to objects in JavaScript. Like objects.
Example;
We will explore the process of constructing a struct and displaying its contents.
struct Person {
name: String,
age: u32,
}
fn main() {
let new_person = Person {
name: String::from("kord.rs formerly kord.js"),
age: 19,
};
println!("{}", new_person.name);
println!("{}", new_person.age);
}
Response
kord.rs formaly kord.js
19
Suppose we aim to utilize the println! macro to print the entire struct, achieved through println!("{}", new_person);
This will result in our output resembling
error[E0277]: `Person` doesn't implement `std::fmt::Display`
--> src/main.rs:323:19
|
323 | println!("{}",new_person);
| ^^^^^^^^^^ `Person` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Person`
= 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 macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error
In this scenario, we observe that utilizing println!({}) to output our code is not viable. A remarkable aspect of Rust is its informative error messages that guide us towards solutions. The initial error message indicates the need to implement the std::fmt::Display trait. This message is essentially suggesting that we implement the trait to enable proper formatting of our custom type for display purposes which brings us to what is std::fmt::Display in rust ๐ซ ๐ซ ๐ซ.
std::fmt::Display:
This is a trait that defines how instances of a type should be formatted when displayed as text. It requires implementing the fmt method, allowing you to use {} placeholders in macros like println! to customize the appearance of your data. This trait is useful for creating user-friendly representations of data.
Since Rust prompts us to implement std::fmt::Display for our struct in order to print out our response, let's explore how to accomplish that. But before we delve into the implementation details, let's first examine what the display implementation looks like.
use std::fmt;
pub trait Display {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
}
It's apparent that the Display trait includes a function named fmt. Any type that implements the Display trait must also implement this function. Now, let's demonstrate how our struct can implement the Display trait.
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Name: {},\nAge: {},", self.name, self.age)
}
}
The code above demonstrates how to implement the fmt::Display trait for a custom Person type (a struct). This implementation defines how instances of Person should be formatted when printed using macros like print!.
Regarding the write! macro:
The write! macro creates formatted text, akin to println! and format!. Unlike println!, write! doesn't instantly print; it writes the formatted content to a specified buffer, offering more control over output destinations.
Overall code looks like
use std::fmt;
struct Person {
name: String,
age: u32,
}
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Name: {},\nAge: {},", self.name, self.age)
}
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
println!("{}", person); // This will use the Display implementation
}
If you're up for the challenge and consider yourself a 10X engineer ๐๐๐, and you'd like to take matters into your own hands, here's how you can do it:
use std::fmt;
struct Person {
name: String,
age: u32,
}
impl Person {
pub fn show(&self)->String{
return format!("Name: {},\nAge: {},", self.name, self.age)
}
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
println!("{}", person.show()); // This will use the Display implementation
}
or
use std::fmt;
struct Person {
name: String,
age: u32,
}
impl Person {
pub fn show(&self){
println!("Name: {},\nAge: {},", self.name, self.age)
}
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
person.show();
}
or
struct Person {
name: String,
age: u32,
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
let formatted = format!("Name: {},\nAge: {},\n", person.name, person.age);
print!("{}", formatted);
}
You might be wondering why you need to repeatedly implement Display or write a considerable amount of code for this task. Well, below is a simplified method to achieve the same result more easily:
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
println!("{:?}",person);
}
Observe the brevity of this approach. Here, I utilize the std::fmt::Debug trait and the derive attribute. The derive attribute instructs the Rust compiler to automatically create the Debug trait's implementation for a struct or enum.
Additionally, I can illustrate how the Debug trait assists us:
use std::fmt;
struct Person {
name: String,
age: u32,
}
impl fmt::Debug for Person {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Person {{ name: {}, age: {} }}", self.name, self.age)
}
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
println!("{:?}",person);
}
Debug vs Display
Debug Trait:
The Debug trait is primarily used for generating human-readable debug output of data structures. It's particularly useful during development and debugging phases, providing detailed information about the internal state of a type. The output produced by the Debug trait is intended for developers and might include technical details such as memory addresses. This trait is automatically implemented using the derive attribute, and it's especially handy for quickly inspecting data structures during debugging sessions.
Display Trait:
On the other hand, the Display trait is focused on creating user-friendly representations of data for end-users. It's used to format instances of a type into a more visually appealing and human-readable format. The output produced by the Display trait is tailored for consumption by people, making it suitable for presentation in user interfaces or logs. Unlike the Debug trait, the Display trait is not automatically derived and requires manual implementation.
Conclusion
Understanding how to work with structs in Rust is crucial for creating organized and encapsulated data structures. The ability to format and display struct data using the Display and Debug traits enhances code readability and aids in effective debugging. Whether you opt for the automatic implementations provided by the derive attribute or prefer to customize the formatting using the write! macro, Rust's approach to struct manipulation offers flexibility and control. With these insights, you're well-equipped to harness the power of Rust's struct system and debugging tools for more efficient and maintainable code development.