๐ก 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.โ
๐ Dynamic dispatch is the process of selecting which implementation of a polymorphic operation (method or function) to call at run time.