Often a Rust program needs the concept of shared features or an interface
. Multiple structs have the same feature, and need to be used in a unified way.
impl
In Rust, we can have interfaces that are shared between multiple structs by using the trait keyword. With impl
, we indicate how those traits are implemented.
Consider this example Rust program—we have an Animal trait, and it is shared between the Bird and Snake structs. On the Animal trait, we get the get_weight
function.
get_weight
function that is based on the Bird's feather count.get_weight
function on the number of scales the snake has.print_weight
function receives an Animal trait instance, and we must use the "dyn" keyword to specify this is a trait.get_weight
functions in print_weight
.trait Animal { fn get_weight(&self) -> u32; } struct Bird { feathers: u32 } impl Animal for Bird { fn get_weight(&self) -> u32 { self.feathers * 2 } } struct Snake { scales: u32 } impl Animal for Snake { fn get_weight(&self) -> u32 { self.scales * 15 } } fn print_weight(arg: &dyn Animal) { // Use Animal trait. println!("WEIGHT: {}", arg.get_weight()); } fn main() { // Create Bird and Snake, use Animal trait. let bird = Bird { feathers: 100 }; print_weight(&bird); let snake = Snake { scales: 50 }; print_weight(&snake); }WEIGHT: 200 WEIGHT: 750
An impl
block can be used without complex features like dyn or traits. We can use impl
to specify "instance methods" on a struct
.
struct Example { colors: usize, } impl Example { fn test(&self) { // Print colors from self. println!("Colors = {}", self.colors); } } fn main() { let ex = Example { colors: 15 }; // Call test function. ex.test(); }Colors = 15
In some programming languages, calling a function on a type instance may be slower than a top-level function. We test this issue in Rust with impl
.
test_value()
function that is not part of an impl
block (a top-level function).impl
version of the test_value()
function that does the same thing.impl
blocks.use std::time::*; fn test_value(value: &str) { if value != "bird" { panic!("ERROR"); } } struct Example { value: String, } impl Example { fn test_value(&self) { if self.value != "bird" { panic!("ERROR"); } } } fn main() { if let Ok(max) = "100000000".parse::<usize>() { let value = "bird".to_owned(); let ex = Example { value: "bird".to_owned(), }; // Version 1: test with top-level function. let t0 = Instant::now(); for _ in 0..max { test_value(&value); } println!("{} ms", t0.elapsed().as_millis()); // Version 2: test with function in impl block. let t1 = Instant::now(); for _ in 0..max { ex.test_value(); } println!("{} ms", t1.elapsed().as_millis()); } }34 ms test_value() 33 ms ex.text_value()
In other programming languages (like C#) the trait keyword is most similar to an interface
. Traits can be used to implement features of object-oriented programming.
With traits, we create rules about how structs can be used in a unified way. So we can call a specific function on multiple struct
types—this can make code clearer and more correct.