Struct
To begin I guess interest to mention Structs due to the uses with Traits, but it also needed to clarify that it is possible to use it with others datatype like Enum.
In the book The Rust Programming Language has a definition of what I think that is wonderful: "A struct, or structure, is a custom data type that lets you name and package together multiple related values that make up a meaningful group". Basically, its data type what contains another data types inside it.
struct Person {
name: String,
age: u8,
}
This is a variation of structs uses, but it is enough to exemplify what is it. If compared with "normal" OO programming language, it is like that object's data while Traits are like behaviors.
There was no deepness here because the main topic it is not Structs it was just to show that Struct is important to Traits understatement, but if you want to know about it, Rust by Example and The Rust Programming Language can help you so much.
Traits
"A trait is a collection of methods defined for an unknown type: Self. They can access other methods declared in the same trait."
Knowing I'm trying to compare Rust with traditional OO programming language, Simplifying too, is enough to say that Traits are a way to share abstract behaviors to datatype. It is like a method and "abstract methods" in OO programming language. While Structs are that data, Traits are the methods.
On my search I found many variants of that phrase: "Rust Traits are like interfaces of others programming languages with some particulate characteristics", what make me have a better understanding of the theme, so I think it is worth to say here again.
Here is a self explained example from Rust By Example book.
struct Sheep { naked: bool, name: &'static str }
trait Animal {
// Static method signature; `Self` refers to the implementor type.
// That will be that "object's constructor"
fn new(name: &'static str) -> Self;
// Instance method signatures; these will return a string.
fn name(&self) -> &'static str;
fn noise(&self) -> &'static str;
// Traits can provide default method definitions.
fn talk(&self) {
println!("{} says {}", self.name(), self.noise());
}
}
// Note what Sheep impl has the same name as Sheep struct and had the basic
// methods for every Sheep
impl Sheep {
fn is_naked(&self) -> bool {
self.naked
}
fn shear(&mut self) {
if self.is_naked() {
// Implementor methods can use the implementor's trait methods.
println!("{} is already naked...", self.name());
} else {
println!("{} gets a haircut!", self.name);
self.naked = true;
}
}
}
// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
// `Self` is the implementor type: `Sheep`.
fn new(name: &'static str) -> Sheep {
Sheep { name: name, naked: false }
}
fn name(&self) -> &'static str {
self.name
}
fn noise(&self) -> &'static str {
if self.is_naked() {
"baaaaah?"
} else {
"baaaaah!"
}
}
// Default trait methods can be overridden.
fn talk(&self) {
// For example, we can add some quiet contemplation.
println!("{} pauses briefly... {}", self.name, self.noise());
}
}
fn main() {
// Type annotation is necessary in this case.
let mut dolly: Sheep = Animal::new("Dolly");
// TODO ^ Try removing the type annotations.
dolly.talk();
dolly.shear();
dolly.talk();
}
Self
The Self keyword, in this case, is a parameter what contains a "reference" to access either the own trait definition or impl blocks. The &
character found here has relation with the Rust memory management system (ownership) and deceive a full blog to it.
It is easy to get it through this piece of code:
struct Sheep { naked: bool, name: &'static str }
// This is a impl block to the Sheep struct
impl Sheep {
// Here is possible acess the data of a Sheep structure definition like
// properties of a class
fn is_naked(&self) -> bool {
self.naked
}
// This is a mutable reference, what means that 'self' can modify the
// data from the struct Sheep
fn shear(&mut self) {
if self.is_naked() {
// Implementor methods can use the implementor's trait methods.
println!("{} is already naked...", self.name());
} else {
println!("{} gets a haircut!", self.name);
// What have made here
self.naked = true;
}
}
}
In the Animal Trait definition and implementation to Sheep has a different thing, the method new
does not have the Self reference like others. It is means that is a Static Method and can be used without a Sheep definition. Normally, the new
is like a constructor, a special method used to define a new struct with basic data.
// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
// `Self` is the implementor type: `Sheep`.
// This one does not use the self, but define and return a Sheep
fn new(name: &'static str) -> Sheep {
Sheep { name: name, naked: false }
}
fn name(&self) -> &'static str {
self.name
}
fn noise(&self) -> &'static str {
if self.is_naked() {
"baaaaah?"
} else {
"baaaaah!"
}
}
// Default trait methods can be overridden.
fn talk(&self) {
// For example, we can add some quiet contemplation.
println!("{} pauses briefly... {}", self.name, self.noise());
}
}
Attribute
The Rust compile provide some default Traits through the standard library. It does that via the #[derive]
attribute added to a datatype. If a more complex behavior is needed, it is possible to add a custom implementation to that Traits.
From Rust by Example
- Comparison traits: Eq, PartialEq, Ord, PartialOrd.
- Clone, to create
T
from&T
via a copy. - Copy, to give a type 'copy semantics' instead of 'move semantics'.
- Hash, to compute a hash from
&T
. - Default, to create an empty instance of a data type.
- Debug, to format a value using the
{:?}
formatter.
// `Centimeters`, a tuple struct that can be compared
#[derive(PartialEq, PartialOrd)]
struct Centimeters(f64);
// `Inches`, a tuple struct that can be printed
#[derive(Debug)]
struct Inches(i32);
impl Inches {
fn to_centimeters(&self) -> Centimeters {
let &Inches(inches) = self;
Centimeters(inches as f64 * 2.54)
}
}
// `Seconds`, a tuple struct with no additional attributes
struct Seconds(i32);
fn main() {
let _one_second = Seconds(1);
// Error: `Seconds` can't be printed; it doesn't implement the `Debug` trait
//println!("One second looks like: {:?}", _one_second);
// TODO ^ Try uncommenting this line
// Error: `Seconds` can't be compared; it doesn't implement the `PartialEq` trait
//let _this_is_true = (_one_second == _one_second);
// TODO ^ Try uncommenting this line
let foot = Inches(12);
println!("One foot equals {:?}", foot);
let meter = Centimeters(100.0);
let cmp =
if foot.to_centimeters() < meter {
"smaller"
} else {
"bigger"
};
println!("One foot is {} than one meter.", cmp);
}
Return Traits
A Trait can be returned for a function. Indeed, a datatype which implement that Trait can be returned. The impl
keyword is added to the function's return type with the Trait.
struct Programmer {}
struct Engineer {}
struct Doctor {}
trait Person {
// default speak method
fn speak(&self) -> String {
format!("The person is speaking...")
}
}
impl Person for Programmer {
fn speak(&self) -> String {
format!("There is a bug here...")
}
}
impl Person for Engineer {
fn speak(&self) -> String {
format!("The bridge is ready")
}
}
impl Person for Doctor {
fn speak(&self) -> String {
format!("Amoxicilina")
}
}
// These functions return a Struct what implement Person Trait
fn create_a_programmer() -> impl Person {
Programmer {}
}
fn create_an_engineer() -> impl Person {
Engineer {}
}
fn create_a_doctor() -> impl Person {
Doctor {}
}
fn main() {
let programmer = create_a_programmer();
let enginner = create_an_engineer();
let doctor = create_a_doctor();
println!("{}", programmer.speak());
println!("{}", enginner.speak());
println!("{}", doctor.speak());
()
}
"Inheritance"
In another analogy with traditional OO programming language and the core concepts of this, "inheritance" or supertraits comes to join different Traits together using the +
signal. Other keyword in this code is dyn
, what has relation with the Rust memory management, what I guess I will Blog about as soon as possible.
Good explanations: Dyn and dyn , impl and Trait Objects — Rust
trait Person {
fn name(&self) -> String;
}
// Person is a supertrait of Student.
// Implementing Student requires you to also impl Person.
trait Student: Person {
fn university(&self) -> String;
}
trait Programmer {
fn fav_language(&self) -> String;
}
// CompSciStudent (computer science student) is a subtrait of both Programmer
// and Student. Implementing CompSciStudent requires you to impl both supertraits.
trait CompSciStudent: Programmer + Student {
fn git_username(&self) -> String;
}
fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
format!(
"My name is {} and I attend {}. My favorite language is {}. My Git username is {}",
student.name(),
student.university(),
student.fav_language(),
student.git_username()
)
}
fn main() {}
That have been the main topics about Traits what I think it is worth trying to simplify and show how I think about this in my learning path. Of course, it does not have so deepness because I am limiting my learning scope to it does not drown me on the complex Rust world and gradually improve myself until a comfortable confident level.
Feel free to coment, correct me or just say a hi. Thanks for reading and I hope that help someone.