💡 When we discussed about C-like structs, I mentioned that those are similar to classes in OOP languages, but without their methods. impls are used to define methods for Rust structs and enums.
💡 Traits are kind of similar to interfaces in OOP languages. They are used to define the functionality a type must provide. Multiple traits can be implemented for a single type.
⭐️️ But traits can also include default implementations of methods. Default methods can be overriden when implementing types.
Impls without traits
struct Player {
first_name: String,
last_name: String,
}
impl Player {
fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
fn main() {
let player_1 = Player {
first_name: "Rafael".to_string(),
last_name: "Nadal".to_string(),
};
println!("Player 01: {}", player_1.full_name());
}
// ⭐️ Implementation must appear in the same crate as the self type
// 💡 And also in Rust, new traits can be implemented for existing types even for types like i8, f64 and etc.
// Same way existing traits can be implemented for new types you are creating.
// But we can not implement existing traits into existing types
Impls & traits, without default methods
struct Player {
first_name: String,
last_name: String,
}
trait FullName {
fn full_name(&self) -> String;
}
impl FullName for Player {
fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
fn main() {
let player_2 = Player {
first_name: "Roger".to_string(),
last_name: "Federer".to_string(),
};
println!("Player 02: {}", player_2.full_name());
}
// 🔎 Other than functions, traits can contain constants and types
⭐️ As you can see methods take a special first parameter, the type itself. It can be either self, &self, or &mut self; self if it’s a value on the stack (taking ownership), &self if it’s a reference, and &mut self if it’s a mutable reference.
Impls with Associated functions
Some other languages support static methods. At such times, we call a function directly through the class without creating an object. In Rust, we call them Associated Functions. we use :: instead of . when calling them from struct. ex. Person::new(“Elon Musk Jr”);
struct Player {
first_name: String,
last_name: String,
}
impl Player {
fn new(first_name: String, last_name: String) -> Player {
Player {
first_name : first_name,
last_name : last_name,
}
}
fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
fn main() {
let player_name = Player::new("Serena".to_string(), "Williams".to_string()).full_name();
println!("Player: {}", player_name);
}
// we have used :: notation for `new()` and . notation for `full_name()`
// 🔎 Also in here we have used `Method Chaining`. Instead of using two statements for new() and full_name()
// calls, we can use a single statement with Method Chaining.
// ex. player.add_points(2).get_point_count();
Traits with generics
trait From<T> {
fn from(T) -> Self;
}
impl From<u8> for u16 {
//...
}
impl From<u8> for u32{
//...
}
//should specify after the trait name like generic functions
Traits inheritance
trait Person {
fn full_name(&self) -> String;
}
trait Employee : Person { //Employee inherit from person trait
fn job_title(&self) -> String;
}
trait ExpatEmployee : Employee + Expat { //ExpatEmployee inherit from Employee and Expat traits
fn additional_tax(&self) -> f64;
}
Trait objects
🔎 While Rust favors static dispatch, it also supports dynamic dispatch through a mechanism called ‘trait objects.’