💡 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
structPlayer { first_name:String, last_name:String,}implPlayer {fnfull_name(&self) ->String {format!("{} {}", self.first_name, self.last_name) }}fnmain() {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
structPlayer { first_name:String, last_name:String,}traitFullName {fnfull_name(&self) ->String;}implFullNameforPlayer {fnfull_name(&self) ->String {format!("{} {}", self.first_name, self.last_name) }}fnmain() {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
Impls, traits & default methods
traitFoo {fnbar(&self);fnbaz(&self) { println!("We called baz."); }}
⭐️ 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”);
structPlayer { first_name:String, last_name:String,}implPlayer {fnnew(first_name:String, last_name:String) ->Player {Player { first_name : first_name, last_name : last_name, } }fnfull_name(&self) ->String {format!("{} {}", self.first_name, self.last_name) }}fnmain() {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
traitFrom<T> {fnfrom(T) -> Self;}implFrom<u8> foru16 {//... }implFrom<u8> foru32{//... }//should specify after the trait name like generic functions
Traits inheritance
traitPerson {fnfull_name(&self) ->String;}traitEmployee:Person { //Employee inherit from person traitfnjob_title(&self) ->String; }traitExpatEmployee:Employee+Expat { //ExpatEmployee inherit from Employee and Expat traitsfnadditional_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.