🧵 Traits.
-
Traits
- Traits in Rust are like interfaces in other languages.
- They define a common interface for a group of types to implement.
- Traits define a set of requirements that types must fulfill to implement the trait.
- Types can implement multiple traits, allowing them to be treated abstractly as any of those traits.
trait ExampleTrait { fn do_something(&self); } struct ExampleStruct {} impl ExampleTrait for ExampleStruct { fn do_something(&self) { println!("I did something!"); } } fn main() { let ex = ExampleStruct{}; ex.do_something(); } // Output: // I did something!
-
Trait & Generics
- When a type implements a trait, it can be treated abstractly as that trait using generics or trait objects.
- Generics allow functions to accept any type that implements a given trait, without knowing the concrete type.
- Trait objects allow types to be treated abstractly as a trait, even when their concrete type is not known at compile time.
fn print<T: std::fmt::Display>(value: T) { println!("{}", value); } fn main() { print("Hello, world!"); } // Output: // Hello, world!
-
Traits Association
- Traits can have associated functions, types, and constants, which are implemented by the type that implements the trait.
- Associated functions are functions that belong to the trait itself, rather than to a specific implementation of the trait.
- Associated types are types that are defined within the trait, and can be used as part of the trait’s definition.
and so on.
trait MyTrait { const MY_CONSTANT: u32; type MyType; fn my_function(&self) -> Self::MyType; } struct MyStruct {} impl MyTrait for MyStruct { const MY_CONSTANT: u32 = 42; type MyType = String; fn my_function(&self) -> Self::MyType { "Hello, world!".to_string() } } fn main() { let my_struct = MyStruct{}; println!("My constant: {}", MyStruct::MY_CONSTANT); println!("My function: {}", my_struct.my_function()); } // Output // My constant: 42 // My function: Hello, world!
-
Traits Inside Traits
- A trait can extend the functionality of another trait by declaring it as a supertrait.
- A type that implements the subtrait must also implement all of its supertraits.
- In the following example, ThreeIterator is a subtrait of
std::iter::Iterator
trait.
trait ThreeIterator: std::iter::Iterator { fn next_three(&mut self) -> Option<[Self::Item; 3]>; } impl ThreeIterator for std::vec::IntoIter<i32> { fn next_three(&mut self) -> Option<[Self::Item; 3]> { let mut res = [0; 3]; for i in 0..3 { if let Some(val) = self.next() { res[i] = val; } else { return None; } } Some(res) } } fn main() { let nums = vec![1, 2, 3, 4, 5]; let mut it = nums.into_iter(); assert_eq!(it.next_three(), Some([1, 2, 3])); }
-
Traits as Parameters
- Traits can be used in function parameters to specify that the argument must implement a particular trait.
- In the following example, the
debug_iter
function accepts anyIterator
where the item type implementsstd::fmt::Debug
.
fn debug_iter<I: Iterator>(it: I) where I::Item: std::fmt::Debug { for elem in it { println!("{:#?}", elem); } } fn main() { let nums = vec![1, 2, 3]; debug_iter(nums.iter()); } // Output // 1 // 2 // 3
-
Traits As Return Types
- Traits can also be used as the return type of a function, allowing the implementation details to be hidden from the user.
- In the following example, the
from_zero_to
function returns anIterator
with items of typeu8
.
fn from_zero_to(v: u8) -> impl Iterator<Item = u8> { (0..v).into_iter() } let nums = from_zero_to(5); assert_eq!(nums.collect::<Vec<u8>>(), vec![0, 1, 2, 3, 4]);
-
Traits Objects
- A trait object is an opaque value of another type that implements a set of traits.
- Trait objects can be used when the concrete type implementing the trait is unknown at compile time.
- Syntax:
dyn BaseTrait + AutoTrait1 + ... AutoTraitN
. - In the following example, a
Box<dyn Print>
is a trait object that can hold any value that implements thePrint
trait.
trait Animal { fn speak(&self); } struct Dog; impl Animal for Dog { fn speak(&self) { println!("Woof!"); } } struct Cat; impl Animal for Cat { fn speak(&self) { println!("Meow!"); } } fn main() { let dog = Dog; let cat = Cat; // create trait objects let animal1: &dyn Animal = &dog; let animal2: &dyn Animal = &cat; animal1.speak(); animal2.speak(); } // Output // Woof! // Meow!
-
Unsafe Traits
- Some traits may be unsafe to implement.
- The
unsafe
keyword is used to mark a trait as unsafe.
unsafe trait UnsafeTrait {} unsafe impl UnsafeTrait for i32 {} fn main() { let n: i32 = 42; let ptr = &n as *const i32; println!("{:p}", ptr); } // Output // 0x7ffc79cba93c
-
Traits 2015 vs 2018
- In the 2015 edition, the parameters pattern was not needed for traits. This behavior is no longer valid in edition 2018.
// 2015 edition trait Tr { fn f(i32); } // 2018 edition trait Tr { fn f(&self, i32); }
-
Inheritance with traits
- Traits can build upon the requirements of other traits.
- This is called “inheritance” with traits.
trait Animal { fn speak(&self); } trait Pet: Animal { fn sit(&self); } struct Dog; impl Animal for Dog { fn speak(&self) { println!("Woof!"); } } impl Pet for Dog { fn sit(&self) { println!("Sitting like a good boy!"); } } fn main() { let dog = Dog; let pet: &dyn Pet = &dog; pet.speak(); pet.sit(); } // Output: // Woof! // Sitting like a good boy!
-
Marker traits
- Traits can serve as markers or carry other logical semantics that aren’t expressed through their items.
- When a type implements that trait, it is promising to uphold its contract.
- Examples of marker traits in the standard library are
Send
andSync
.
struct MyType; unsafe impl Send for MyType {} fn main() { let my_type = MyType; // do something with my_type }
You can refer to this twitter thread for more info.