I have a small little example that I am trying to understand. As far as I understand it a trait lets me to call certain functions on an object for which it is implemented for. Let say I have an object:
struct XX {
x:String
}
impl XX {
fn new(S: String)->Self{
XX{
x:S
}
}
}
and now i want to add:
pub trait Y {
fn get(&self);
}
impl Y for XX{
fn get(&self){
println!("{}", self.x)
}
}
fn main(){
let a = XX::new("bla".to_string());
a.get()
}
So now my question is why would I ever do that ? Why not just make it the core object function since i need the construct the object anyway... Where is the gain? (Point is, I know how to write them but have no clue why would i use them ) Any help?
PS
is is possible to abstract the object creation through them or something?
When you called this, this expanded to a call to the trait method std::fmt::Display::fmt. Thanks to this, you are not just limited to printing Strings, but rather you can print any type that implements Display.
If you have ever programmed in Java or C#, Traits would have a similar utility to what they call Interfaces (although traits are more composable/powerful).
If you only ever plan on having a single implementation of something, then you are right, traits would not really be too helpful.
However, imagine for example, something like a UI graphics library, where you would have different/unique components, but they would all share the same ability to be displayed on the UI.
Here is a semi convoluted example below that hopefully can illustrate the value:
fn main() {
let checkbox = Checkbox { is_checked: true };
let dropdown = DropdownMenu { choices: vec!["Do".to_string(), "Rei".to_string()] };
display_on_ui(checkbox);
display_on_ui(dropdown);
}
fn display_on_ui<T: UiComponent>(component: T) {
println!("About to display a component on the UI!");
component.render();
}
trait UiComponent {
fn render(&self);
}
struct Checkbox {
is_checked: bool
}
struct DropdownMenu {
choices: Vec<String>
}
impl UiComponent for Checkbox {
fn render(&self) {
if self.is_checked {
println!("CHECKED");
} else {
println!("UNCHECKED");
}
}
}
impl UiComponent for DropdownMenu {
fn render(&self) {
for choice in &self.choices {
println!("{}", choice);
}
}
}
Normally we think about the code we are writing, about some object we are creating and what methods it may require. What functionality we have to expose through those methods.
But if we look at it from the other end of the telescope somebody, somewhere, is creating some code X and wants to define what methods other objects should have so that X can make use of them. Even other objects that don't even exist yet. They want to specify that if you want your object to be usable by X then it must provide some methods that X likes.
That definition of what those unknown/unwritten objects should provide is a "trait."
In other worlds an "interface".
So, now that you know what X needs, because it has specified the trait. Then you can provide an implementation of that trait for your new object.
Someone correct me if I have the wrong end of the stick here.
Thank you so much for the examples and elaborattions. In conclusion one would say that the traits can be used for :
a) different implementations of the same method for different objects (e.g.)
b) as a "parser" for different types passed or retrieved from a given object
Which is brilliant in case of large code base. Which brings another question: Why not implement all object methods as traits (from constructor all the way to a destructor) sincenecessity this opens the object for further re-implementations down the road without the need to re-write the whole code base (or mess with it), right?
I'm just guessing here, but I think the answer comes down to two things:
Compile times. If the compiler has to resolve all those imports and try to optimize all the dynamic dispatch calls where it can be statically dispatched, compile times will shoot through the roof. Compile times are already one of the reasons people give which is a pain about working with Rust, so this is a no-go.
Alternatively, or as well, this will make code slower, because runtime dispatch will be necessary either to keep compile times in check or just inevitably with some designs.
You'll only suffer the cost of dynamic dispatch when you use trait objects (dyn Trait). Generic types (T: Trait) get resolved at compile time, which means you pay the cost in longer compilation times and larger binaries, but not in program execution speed.