impl
blocks in RustRust generic implementation blocks make implementing methods for generic types much easier.
In this article, we’ll illustrate the value of generic implementation blocks by first understanding the tediousness of a world without generics, and the results from when we use them.
To jump ahead in this article:
impl
blocksimpl
blocksimpl
blocksUnlike languages like C++, JavaScript, and C#, in Rust, you don’t have objects (groups of properties and methods) or classes for defining types of objects.
Instead, Rust, like Go, opts for structs that are just groups of properties. In Rust, methods are added to structs by using implementation, and impl
blocks are used for defining methods. Refer to the example below.
// Struct Definition struct Person { name: String } // Impl block defining method impl Person { fn say_name(&self){ println!("{}", self.name); } } fn main() { // Instantiate a person let alex = Person {name: "Alex Merced".to_string()}; // Call methods alex.say_name(); }
In the code above, we created a Person
struct with one String
property called name
. Then, we used an impl
block to define and implement a say_name
method. In the main function, we created a Person
struct and called the say_name()
method, which will print Alex Merced
.
Sometimes, we have structs that may have properties whose types are defined differently. To designate that, we can list out the unknown types as variables like <T>
or <T, U>
, depending on how many variable types we need to designate.
Then, we need to implement any methods for each type we want to support. This can be very tedious if we are implementing a method for many types.
// Generic Struct Definition, T is a variable for unknown type struct Stats<T> { age: T, height:T } // Impl blocks defining methods for different types impl Stats<i32> { fn print_stats(&self){ println!("Age is {} years and Height is {} inches", self.age, self.height); } } impl Stats<f32> { fn print_stats(&self){ println!("Age is {} years and Height is {} feet", self.age, self.height); } } fn main() { // Instantiate using i32 stats let alex = Stats {age: 37, height: 70}; // Instantiate using f32 stats let alex2 = Stats {age: 37.0, height: 5.83}; // Call methods alex.print_stats(); alex2.print_stats(); }
In the example code above, we define a Stats
struct, which has two properties of the same type, denoted by T
. Because we defined it as a generic type when we wrote <T>
, this type can be anything, but it must be the same for age and height.
We want any Stats
objects that use two 32 bit numbers, whether integers or floats, to have a print_stats
method. In this case, we have to create two implementation blocks for each possibility.
We then instantiate two Stats
structs to use i32
values and f32
values. We call the print_stats
method for structs to get the following output:
Age is 37 years and Height is 70 inches Age is 37 years and Height is 5.83 feet
Notice the different output because we called the implementation from the right implementation block.
Writing a separate implementation block for each type can get tedious, especially if there are several possibilities.
If the implementation for several types is going to be identical, we can use generic implementation blocks, which, like generic types, allow us to define variables representing the type:
use std::fmt::Display; // Generic Struct Definition, T is a variable for unknown type struct Stats<T> { age: T, height:T } // Impl blocks defining methods for different types impl<T:Display> Stats<T> { fn print_stats(&self){ println!("Age is {} years and Height is {}", self.age, self.height); } } fn main() { // Instantiate using i32 stats let alex = Stats {age: 37, height: 70}; // Instantiate using f32 stats let alex2 = Stats {age: 37.0, height: 5.83}; // Instantiate using String stats let alex3 = Stats {age: "37".to_string(), height: "5'10ft".to_string()}; // Call methods alex.print_stats(); alex2.print_stats(); alex3.print_stats(); }
In the code above, we imported the Display
trait from the standard library because we’ll need to reference it later.
We defined the Stats
struct as a struct with two properties of the same generic type, age, and height, respectively.
We used only one implementation block to define a generic type of <T:Display>
, which means T
is the variable for the generic type. :Display
means it must be a type that implements the Display
trait. This is required so we can use the println!
macro in this implementation.
We then defined three structs: one using i32
, another using f32
, and the final one using strings as its property values. (We used three different types and only needed one impl
block. How cool is that!)
When it runs, you should get the output below:
Age is 37 years and Height is 70 Age is 37 years and Height is 5.83 Age is 37 years and Height is 5'10ft
impl
blocksGeneric impl
blocks can also be helpful when we need to implement a trait on many types. In this scenario, we can use a generic implementation to save us time, then define specific implementations as needed. You can see this in the code below.
use std::fmt::Display; // Generic Struct Definition, T is a variable for unknown type struct Stats<T> { age: T, height:T } // Generic Tuple Struct that holds one value struct Number<T> (T); // trait with default implementation of print_stats method trait ViewStats { fn print_stats(&self){ println!("The type of age and height doesn't implement the display trait") } } // Impl blocks defining methods for different types impl<T:Display> ViewStats for Stats<T> { fn print_stats(&self){ println!("Age is {} years and Height is {}", self.age, self.height); } } //Impl block to ViewStats trait to number but use default implementation impl<T> ViewStats for Stats<Number<T>> {} fn main() { // Instantiate using i32 stats let alex = Stats {age: 37, height: 70}; // Instantiate using f32 stats let alex2 = Stats {age: 37.0, height: 5.83}; // Instantiate using String stats let alex3 = Stats {age: "37".to_string(), height: "5'10ft".to_string()}; // Instantiate using String stats let alex4 = Stats {age: Number(37), height: Number(70)}; // Call methods alex.print_stats(); alex2.print_stats(); alex3.print_stats(); alex4.print_stats(); }
In the code above, we imported the Display
trait from the standard library. We defined the Stats
struct as a struct with properties of the age
and height
of any types, except matching types.
Then, we defined a Number
struct, which is a tuple with one value. This is just so we can demonstrate what happens when we create stats with a type that doesn’t implement display.
Next, we defined a ViewStats
trait where the print_stats
method is given a default implementation. This implementation will be called if the values of age
and height
are valid types but don’t have their own implementation.
Then, we defined an implementation of ViewStats
for Stats<T>
, where T
is any type that implements the Display
trait, like i32
, f32
, and String
.
Then, we implemented ViewStats
for Number
. Because we don’t define a new version of print_stats
, it will use the default one from the trait declaration block.
We then created four structs. The first three are the same as before, and the fourth one is a Stats
struct where age and height are represented by Number
structs.
When we run the script, we get the following output:
Age is 37 years and Height is 70 Age is 37 years and Height is 5.83 Age is 37 years and Height is 5'10ft The type of age and height doesn't implement the display trait
Notice that the final line shows the result of our default implementation from the train block because Number<T>
doesn’t implement display, so it can’t use the same implementation as the first three instances of Stats<T>
.
Generics simplify writing code that should work with many types, whether it is by using generics to define property types or generic impl
blocks to define methods without the tediousness of doing so repeatedly.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]