What is a trait?
A trait is a way to enable reuse methods. We can think about traits that have some of the features of interfaces and abstract classes, and also have other features that these don’t have. We will cover all of these in this blog post.
Rust doesn’t have fully object oriented programming methodologies (for example inheritence). But Rust has different methods for reaching same things. Traits are one of them. If you’re coming from OOP background, then you can thing traits like abstract classes. In a trait you can create methods with implementations and method definitions without implementation. Empty methods will be impement by structs or enums which uses that trait. In OOP paradigm we create object hierarchies like a tree through inheritence. But in Rust we can’t create multi level objects because there isn’t inheritence. But we can create structs which implements traits. This means we can’t create vertical tree structured classes but only horizontal. If you got used to solve problems by tree structured classes through inheritence, then may be you stagger when understanding Rust but don’t worry about. Although Rust doesn’t have natural inheritence you can continue to thinking in inheritence paradigm. In here we will show you how to do that.
Deriving traits to structs/enums
In Rust we can combine traits with structs and enums. With this way we can bring trait properties and methods to structs. This logic is same as interface or abstract class but not exactly.
Standard Library Traits
- Default: This trait’s purpose is creating a new struct with default values. For example you have a User struct. If you want to create a new User struct with default values you must implement this trait. Example code:
#[derive(Debug, Clone)] struct User { id: u32, firstname: String, lastname: String, email: String, password: String, } impl Default for User { fn default() -> Self { User { id: 0, firstname: String::from(""), lastname: String::from(""), email: String::from(""), password: String::from(""), } } } let u1 = User::default(); println!("User firstname: {}", u1.firstname); let u2 = User { id: 2, ..User::default() // Double dot is range operator for extracts every property from a struct. // Also this syntax is called as "struct update syntax". }; println!("User 2 id: {}", u2.id);
- PartialEq / Eq: PartialEq is a marker trait that you can implement if the equality logic is reflexive, transitive and symmetric. Eq trait is similar. Which one must you use? If you need full equality then you must implement Eq, othersize PartialEq. After implementing them you can use orig.eq(&other) method and equals operator
==
.
impl PartialEq for User { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for User {} let u1 = User::default(); let u2 = User { id: 2, ..User::default() }; let u3 = User { id: 2, firstname: String::from("Emir"), ..User::default() }; println!("u1 == u2 {} {}", u1.eq(&u2), u1 == u2); // false false println!("u2 == u3 {} {}", u2.eq(&u3), u2 == u3); // true true
PS: &self and self: &Self are same. These are means immutable reference to this struct.
- From / Into: These two are interesting traits. If you implement From then Into is automatically implemented for you. So you must always implement From, so you get both. However, it’s more idiomatic to use the Into trait in generic functions. PS: Take care of how you implement From trait. If you implement it with ownership move then you can’t use variable again. You must implement them as immutable reference. Examples are here:
// Implementation of ownership move impl From<User> for String { fn from(user: User) -> String { format!("User: {{{} {}}}", user.id, user.firstname) } } ... println!("{}", String::from(u3)); // u3 moved here // u3 is not accessible anymore. // Implementation of immutable reference impl From<&User> for String { fn from(user: &User) -> String { format!("User: {} {}", user.id, user.firstname); } } ... println!("{}", String::from(&u3));
Happy coding!
—————-
0 yorum