JavaScript is one of the languages that do not need to declare the type. The data type is automatically implied and can change flexibly. A variable initially assigned a value as a number, just after a few lines of code it can become a string or any other value. That's interesting and sometimes a disaster!
In Rust, as well as in many typed programming languages, we are forced to declare the type for almost everything like variables, functions... In short, the data type is mandatory and you cannot change from one type to another like in JavaScript.
This has both advantages and disadvantages. When everything is clear, we can avoid many unnecessary errors, and it's also easier for others to understand what an object holds. But on the other hand, you have to spend a lot of time declaring types and lose the flexibility like in JavaScript.
Let's put that aside for now, because the above is just a personal opinion. Whether you like it or not depends on each individual and each situation. But Rust is definitely a typed language, and you must know about Generic and Traits to know how to handle some common cases.
We use generic to create definitions for functions or structs, and then use them with specific data types.
Take a simple example, largest
is a function that finds the largest number in a list
with the type i32
.
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
largest
takes a list
with the type i32
, and after finding the largest number, it returns that number. But in reality, besides i32
, we have many other data types that can find the largest value like i64
, f32
, f64
, char
... Obviously, largest
cannot take a list
with a different data type than i32
. Do we have to create another function with a new data type every time we want to find the largest number?
Generics in Rust help us solve this problem of code duplication. A Generic is declared with uppercase letters to represent a general value. For example:
fn largest<T>(list: &[T]) -> &T {
...
}
It's easy to see that T
here represents a general value instead of a specific data type. If we read it, it would be: "the largest
function takes a list
with the data type T
and returns a value also with the data type T
".
The new function implementation will look like this:
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
However, largest
still cannot work because T
is too general. Let's look at the function body, the comparison item > largest
makes the function not work if we pass in data types that cannot be compared like struct
, enum
... To solve this, in Rust, we have the concept of traits - PartialOrd
is a trait and we only need to declare T
as PartialOrd
.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
So what is a trait?
Traits are a concept to group similar objects. Traits are similar to interfaces in object-oriented programming languages.
For example, you have two objects NewsArticle
used to store a news article and Tweet
to store a discussion topic. You realize that both objects need a summarize
function to summarize the content instead of displaying a long content. Traits appear:
pub trait Summary {
fn summarize(&self) -> String;
}
Summary
is declared as a trait and inside there is a method summarize
. Looking at it, we only see that it returns a String
and does not see any implementation code. That's right, because this is just a trait declaration.
Assuming NewsArticle
and Tweet
have the following structures:
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
Then to implement the trait, we need to do:
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
As a result, whenever we call the summarize
function from NewsArticle
or Tweet
, we will get the corresponding result.
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize()); // 1 new tweet: horse_ebooks: of course, as you probably already know, people
}
Why not implement the summarize
function directly in NewsArticle
or Tweet
but have to go through traits? It's to declare the type and use it to support parameters with multiple data types in a function.
Like the example above, when we didn't declare T
as PartialOrd
, the comparison inside would fail. On the other hand, T
is PartialOrd
, PartialOrd
implements the comparison for T
so Rust knows how to compare T
with each other. Or like the example below:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
notify
takes an item
parameter that is the type that implements the Summary
trait. item
can be any object that implements the Summary
trait, then item.summarize()
will work.
Hello, my name is Hoai - a developer who tells stories through writing ✍️ and creating products 🚀. With many years of programming experience, I have contributed to various products that bring value to users at my workplace as well as to myself. My hobbies include reading, writing, and researching... I created this blog with the mission of delivering quality articles to the readers of 2coffee.dev.Follow me through these channels LinkedIn, Facebook, Instagram, Telegram.
Comments (0)