This blog offers a quick tour of Rust struct
s, method
s and trait
s. It uses simple examples to demonstrate the concepts.
the code is available on GitHub
struct
A struct
in Rust is the same as a Class
in Java or a struct
in Golang. Its a named type to which you can assign state (attributes/fields) and behavior (methods/functions).
Here is a struct
with fields
struct Programmer {
email: String,
github: String,
blog: String,
}
To instantiate a Programmer
, you can simply:
let pg1 = Programmer {
email: String::from("abhirockzz@gmail.com"),
github: String::from("https://github.com/abhirockzz"),
blog: String::from("https://dev.to/abhirockzz"),
};
Methods
Methods are behavior associated with a given type. The first parameter in a method is always self
, which represents the instance on which the method is being invoked.
Let's add a method to Programmer
. To do that, we will need use an impl
block:
impl Programmer {
fn is_same_as(&self, other: Programmer) -> bool {
return self.email == other.email;
}
}
The is_same_as
method accepts a reference to the instance being invoked on (&self
) and another Programmer
instance. To call it, create another instance of a Programmer
(pg2
) and compare pg1
with it.
let pg2 = Programmer {
email: String::from("abhirockzz@gmail.com"),
github: String::from("https://github.com/abhirockzz"),
blog: String::from("https://medium.com/@abhishek1987"),
};
println!("pg1 same as pg2? {}", pg1.is_same_as(pg2));
self types
We used &self
as the parameter for is_same_as
. This way, we will only pass a reference and the function will not own the value - only borrow it (see Rust: Ownership and Borrowing). It is also possible to use self
and &mut self
.
You can use self
, but be careful that it will pass on the ownership to the function. Let's see this function
fn details(self) {
println!(
"Email: {},\nGitHub repo: {},\nBlog: {}",
self.email, self.github, self.blog
)
}
You can invoke it using the pg2
instance as such pg2.details();
and you should get back
Email: abhirockzz@gmail.com,
GitHub repo: https://github.com/abhirockzz,
Blog: https://medium.com/@abhishek1987
If you try to use pg2
again (e.g. pg2.is_same_as(&pg1);
), you will get a compiler error
error[E0382]: borrow of moved value: `pg2`
If you want to mutate the Programmer
instance, make use of &mut self
as follows:
fn some_function(&mut self) {
//use self
}
//to invoke it
let mut pg3 = Programmer{...};
pg3.some_function();
If you don't mark the variable (pg3
) as mut
(mutable), you'll get a compiler error
error[E0596]: cannot borrow `pg2` as mutable, as it is not declared as mutable
Other functions related to the struct
You can add associated functions
which are tied with the instance of a struct
- think of it as a static
method in Java. These are commonly used as constructors
it's different to how constructors are used in Java but similar to Go approach
fn new(email: String, github: String, blog: String) -> Self {
return Programmer {
email: email,
github: github,
blog: blog,
};
}
Here is how you can use it:
let pg3 = Programmer::new(
String::from("abhirockzz@gmail.com"),
String::from("https://github.com/abhirockzz"),
String::from("https://medium.com/@abhishek1987"),
);
Notice that we are using ::
(Programmer::new
) to access associated
members of a struct
(a function in this case)
It is possible to use different
impl
blocks for the samestruct
Trait
A Trait
in Rust is similar to Interface
in other languages such as Java etc. They help define one or more set of behaviors which can be implemented
by different types in their own unique way. The way a Trait
is implemented in Rust is quite similar to how it's done in Java. In Java, you can use the implements
keyword, which Rust uses impl
There is an explicit association b/w the interface and the type implementing it. This is quite different compared to Go, where you don't need to declare which interface you're implementing - if you have implemented the required behavior, the compiler will be happy.
Let's start by defining a Trait
trait PrettyPrint {
fn pretty_print(&self);
}
.. and implementing it
don't worry about the specifics of the example, just focus on the key parts
impl PrettyPrint for Programmer {
fn pretty_print(&self) {
println!(
"{{\n\t\"email\": {},\n\t\"github_repo\" repo: {},\n\t\"blog_url\": {}\n}}",
self.email, self.github, self.blog
)
}
}
It's quite simple - use the impl
keyword followed by the trait you're implementing and include the type which is implementing the trait after the for
keyword. Now you can use it just like any other method:
let pg = Programmer::new(...);
pg.pretty_print();
Use a std
library trait
Rust standard library provides a number of Trait
s which you can use. Let's implement the std::fmt::Display
trait. This is for user-facing output e.g. we can use it with the println!
macro
impl std::fmt::Display for Programmer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {}, {})", self.email, self.github, self.blog)
}
}
Don't worry about the specifics of the implementation and the types such as
std::fmt::Formatter
etc. For now, just understand that you can implementDisplay
trait from the Ruststd::fmt
module.
From now on, you can use an instance of a Programmer
with println!
as such
let another_pg = Programmer::new(...);
println!("programmer details: {}", another_pg);
//output:
programmer details: (abhirockzz@gmail.com, https://github.com/abhirockzz, https://medium.com/@abhishek1987)
Freebies!
Rust provides many useful traits that you can use for free! This means that you don't need to implement them explicitly (you certainly can if you want to). The way you do it is by using the #[derive]
attribute.
An
attribute
is just a piece of metadata that you can apply to structs etc. They remind me of Java annotations
Here is an example. We can use apply the std::fmt::Debug
trait on Programmer
and then use it with println!
. This is similar to what we did with Display
but the key difference is that its not possible to derive
the Display
(you have to implement it). All you need to do is add #[derive(Debug)]
to the the Programmer
struct:
#[derive(Debug)]
struct Programmer {
email: String,
github: String,
blog: String,
}
Simply use the :?
format to leverage the default Debug
functionality
let pg = Programmer::new(...);
println!("{:?}", pg);
//output:
Programmer { email: "abhirockzz@gmail.com", github: "https://github.com/abhirockzz", blog: "https://medium.com/@abhishek1987" }
If you add a #
to the mix, you get pretty printing for free.. yay!
println!("{:#?}", pg);
//output
Programmer {
email: "abhirockzz@gmail.com",
github: "https://github.com/abhirockzz",
blog: "https://medium.com/@abhishek1987",
}
There are other utility traits such as
Eq
,Clone
,PartialEq
etc. which you canderive
Using traits
To take advantage of traits, you should be able to accept and return them from functions and methods in order to make use of the "general" behavior. We can write a function which accepts a type that implements PrettyPrint
fn print_the_printable(p: impl PrettyPrint) {
p.pretty_print()
}
//invoke
let pg = Programmer::new(...);
print_the_printable(pg);
note that
impl
has been added to the parameter
A PrettyPrint
can be returned as well
fn get_printable(info: Vec<String>) -> impl PrettyPrint {
Programmer::new(
String::from(&info[0]),
String::from(&info[1]),
String::from(&info[2]),
)
}
//invoke
let info: Vec<String> = vec![
String::from("abhirockzz@gmail.com"),
String::from("https://github.com/abhirockzz"),
String::from("https://medium.com/@abhishek1987"),
];
get_printable(info).pretty_print();
Default methods
It is possible to provide default implementations of methods within the trait itself (these can be overridden if required). These methods can also invoke other trait methods (default or not)
Top comments (0)