What are Traits in Rust?
A trait in Rust is a collection of methods that define shared functionality. Any type that implements a trait must provide the functionality defined by the trait. Traits are useful for enabling polymorphism, making your code more modular and reusable.
Key Features of Traits
- Abstraction: Traits define behavior without specifying implementation.
- Polymorphism: Traits allow working with multiple types that share common behavior.
- Static Dispatch: Rust uses traits with zero runtime cost by resolving implementations at compile time.
- Code Reuse: Traits enable shared functionality across multiple types.
Defining a Trait
A trait is defined using the trait keyword.
Example:
trait Greet {
fn greet(&self);
}
Implementing a Trait
To implement a trait for a struct, use the impl
keyword.
Example:
trait Greet {
fn greet(&self);
}
struct Person {
name: String,
}
impl Greet for Person {
fn greet(&self) {
println!("Hello, my name is {}.", self.name);
}
}
fn main() {
let person = Person {
name: String::from("Alice"),
};
person.greet();
}
Output:
Hello, my name is Alice.
Traits with Default Method Implementations
Traits can include default implementations for methods, which allows types to use the default behavior or override it.
Example:
trait Greet {
fn greet(&self) {
println!("Hello!");
}
}
struct Dog;
impl Greet for Dog {}
fn main() {
let dog = Dog;
dog.greet(); // Uses the default implementation
}
Output:
Hello!
Traits with Multiple Methods
A trait can define multiple methods.
Example:
trait Calculator {
fn add(&self, a: i32, b: i32) -> i32;
fn multiply(&self, a: i32, b: i32) -> i32;
}
struct Math;
impl Calculator for Math {
fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
fn multiply(&self, a: i32, b: i32) -> i32 {
a * b
}
}
fn main() {
let math = Math;
println!("Addition: {}", math.add(5, 10));
println!("Multiplication: {}", math.multiply(5, 10));
}
Output:
Addition: 15
Multiplication: 50
Trait Bounds
Trait bounds allow you to specify that a generic type must implement a specific trait. This ensures that only types with the required behavior can be used.
Example:
trait Printable {
fn print(&self);
}
struct Book {
title: String,
}
impl Printable for Book {
fn print(&self) {
println!("Book: {}", self.title);
}
}
fn print_item<T: Printable>(item: T) {
item.print();
}
fn main() {
let book = Book {
title: String::from("Rust Programming"),
};
print_item(book);
}
Output:
Book: Rust Programming
Trait Objects
Trait objects allow you to use dynamic dispatch to work with multiple types that implement the same trait. They are defined using Box<dyn Trait>.
Example:
trait Sound {
fn make_sound(&self);
}
struct Cat;
struct Dog;
impl Sound for Cat {
fn make_sound(&self) {
println!("Meow!");
}
}
impl Sound for Dog {
fn make_sound(&self) {
println!("Woof!");
}
}
fn main() {
let animals: Vec<Box<dyn Sound>> = vec![
Box::new(Cat),
Box::new(Dog),
];
for animal in animals {
animal.make_sound();
}
}
Output:
Meow!
Woof!
Deriving Traits
Rust provides built-in traits like Debug, Clone and PartialEq, which can be automatically implemented for your types using the #[derive] attribute.
Example:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 5, y: 10 };
println!("{:?}", point);
}
Output:
Point { x: 5, y: 10 }