Traits in Rust
December 22, 2025In this post, we'll explore traits in Rust - a powerful way to define shared behavior across different types, enabling polymorphism and code reuse.
Before diving into traits, make sure you understand:
- Generics - Writing flexible, reusable code with type parameters
Once you've mastered traits, continue your learning journey with:
- Lifetimes - Ensuring references remain valid throughout your program
Before We Start: What Problem Are We Solving?
Remember at the end of Generics, we had a problem?
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { // ERROR!
largest = item;
}
}
largest
}
Rust refuses to compile this. It says:
error: binary operation `>` cannot be applied to type `T`
Why?
Because T can be ANY type. And not every type can be compared with >.
Think about it:
- Numbers can be compared:
5 > 3✓ - Characters can be compared:
'z' > 'a'✓ - But what about a network connection? Is one connection "greater than" another? That doesn't make sense.
Generics are TOO flexible here. We need to say:
"I want any type... BUT it must be a type that can be compared."
This is exactly what traits do.
Step 1: What Is a Trait?
A trait defines a capability: something a type can DO.
Real-World Analogy
Think about job requirements.
If you're hiring, you don't just say "I need a person." You say:
- "I need someone who can write code"
- "I need someone who can speak Spanish"
- "I need someone who can drive a truck"
These are capabilities. Different people have different capabilities. A software engineer can write code. A translator can speak Spanish. A trucker can drive a truck.
Traits work the same way:
- "I need a type that can be compared"
- "I need a type that can be printed"
- "I need a type that can be copied"
These are capabilities that types can have.
Another Analogy
Think about electrical plugs.
Different devices have different plugs:
- Your phone charger has a USB plug
- Your laptop has a different plug
- Your toaster has another plug
But they all share one capability: they can connect to a power outlet and receive electricity.
If you're building a power strip, you don't care WHAT device gets plugged in. You just care that it HAS A PLUG.
A trait is like saying "has a plug", it's a capability that different types can share.
Step 2: Defining a Simple Trait
Let's create our first trait. We'll start with something simple.
The Scenario
Imagine we're building a program that displays information about different things:
- Animals (show what sound they make)
- Vehicles (show what sound they make)
- Musical instruments (show what sound they make)
They're all different types, but they share one capability: they can make a sound.
Defining the Trait
trait MakesSound {
fn sound(&self) -> String;
}
That's it! We've defined a trait.
Breaking It Down
trait MakesSound {
//^^^
//Keyword to define a trait
trait MakesSound {
// ^^^^^^^^^^
// The name of our trait
trait MakesSound {
fn sound(&self) -> String;
// ^^
// This trait has a method
}
trait MakesSound {
fn sound(&self) -> String;
// ^^^^^
// Method name
}
trait MakesSound {
fn sound(&self) -> String;
// ^^^^^
// Takes a reference to self
}
trait MakesSound {
fn sound(&self) -> String;
// ^^^^^^
// Returns a String
}
trait MakesSound {
fn sound(&self) -> String;
// ^
// Semicolon! No body!
// We only define the signature, not the implementation
}
What This Means
The trait says:
"Any type that implements
MakesSoundMUST have a method calledsoundthat takes&selfand returns aString."
It's a contract. A promise. If a type says "I implement MakesSound," it MUST provide this method.
Why No Method Body?
The trait only defines WHAT the method looks like (its signature). It doesn't say HOW the method works.
Why? Because different types will implement it differently:
- A
Dogmight return"Woof!" - A
Catmight return"Meow!" - A
Carmight return"Vroom!"
The trait just says "you must have this method." Each type decides what the method actually does.
Step 3: Implementing a Trait on a Type
Now let's make some types that implement our MakesSound trait.
Creating a Dog That Makes Sound
First, we define a struct:
struct Dog {
name: String,
}
Now we implement the trait for this struct:
impl MakesSound for Dog {
fn sound(&self) -> String {
String::from("Woof!")
}
}
Breaking Down the impl Block
impl MakesSound for Dog {
//^^
//Keyword for implementing
impl MakesSound for Dog {
// ^^^^^^^^^^
// The trait we're implementing
impl MakesSound for Dog {
// ^^^
// Keyword connecting trait to type
impl MakesSound for Dog {
// ^^^
// The type that gets the trait
impl MakesSound for Dog {
fn sound(&self) -> String {
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// We MUST provide this method (the trait requires it)
String::from("Woof!")
// ^^^^^^^^^^^^^^^^^^^^^
// The actual implementation, what this method DOES for Dog
}
}
The Full Example
trait MakesSound {
fn sound(&self) -> String;
}
struct Dog {
name: String,
}
impl MakesSound for Dog {
fn sound(&self) -> String {
String::from("Woof!")
}
}
fn main() {
let my_dog = Dog { name: String::from("Buddy") };
println!("{} says: {}", my_dog.name, my_dog.sound());
// Output: Buddy says: Woof!
}
Step 4: Multiple Types, Same Trait
The power of traits is that DIFFERENT types can share the SAME capability.
Adding More Types
trait MakesSound {
fn sound(&self) -> String;
}
// A Dog
struct Dog {
name: String,
}
impl MakesSound for Dog {
fn sound(&self) -> String {
String::from("Woof!")
}
}
// A Cat
struct Cat {
name: String,
}
impl MakesSound for Cat {
fn sound(&self) -> String {
String::from("Meow!")
}
}
// A Car
struct Car {
model: String,
}
impl MakesSound for Car {
fn sound(&self) -> String {
String::from("Vroom!")
}
}
Using Them
fn main() {
let dog = Dog { name: String::from("Buddy") };
let cat = Cat { name: String::from("Whiskers") };
let car = Car { model: String::from("Toyota") };
println!("Dog says: {}", dog.sound()); // Woof!
println!("Cat says: {}", cat.sound()); // Meow!
println!("Car says: {}", car.sound()); // Vroom!
}
What's Happening?
Three completely different types:
Dog(an animal with a name)Cat(an animal with a name)Car(a vehicle with a model)
But they ALL have the sound() method because they ALL implement MakesSound.
Each one implements it DIFFERENTLY:
Dog::sound()returns "Woof!"Cat::sound()returns "Meow!"Car::sound()returns "Vroom!"
Same method name. Same signature. Different behavior. That's the power of traits!
Step 5: A Type Can Have Multiple Traits
A type isn't limited to one trait. It can implement many!
Example
trait MakesSound {
fn sound(&self) -> String;
}
trait HasLegs {
fn leg_count(&self) -> u32;
}
struct Dog {
name: String,
}
// Dog implements MakesSound
impl MakesSound for Dog {
fn sound(&self) -> String {
String::from("Woof!")
}
}
// Dog ALSO implements HasLegs
impl HasLegs for Dog {
fn leg_count(&self) -> u32 {
4
}
}
fn main() {
let dog = Dog { name: String::from("Buddy") };
println!("{} says: {}", dog.name, dog.sound()); // Buddy says: Woof!
println!("{} has {} legs", dog.name, dog.leg_count()); // Buddy has 4 legs
}
Key Point
Dog now has TWO capabilities:
- It can make a sound (
MakesSound) - It has legs (
HasLegs)
You can implement as many traits as you want on a type.
Step 6: Traits as Parameters: The Real Power
Here's where traits become incredibly useful.
Remember the problem? We wanted a generic function, but generics were too flexible. Now we can combine generics WITH traits.
The Scenario
We want a function that takes ANYTHING that makes a sound and prints that sound.
Without Traits (The Problem)
fn print_sound<T>(item: &T) {
println!("Sound: {}", item.sound()); // ERROR!
}
This doesn't work because Rust doesn't know if T has a sound() method. T could be ANYTHING, even types that don't make sounds.
With Traits (The Solution)
fn print_sound<T: MakesSound>(item: &T) {
println!("Sound: {}", item.sound()); // Works!
}
Now it works! Let's break down this new syntax.
Breaking Down <T: MakesSound>
fn print_sound<T: MakesSound>(item: &T)
// ^^^^^^^^^^^^^^^
// This is called a TRAIT BOUND
fn print_sound<T: MakesSound>(item: &T)
// ^
// T is a type parameter (generic)
fn print_sound<T: MakesSound>(item: &T)
// ^
// The colon means "where T must implement..."
fn print_sound<T: MakesSound>(item: &T)
// ^^^^^^^^^^
// The trait that T must implement
What This Means
<T: MakesSound> says:
"T can be any type, BUT it must implement the
MakesSoundtrait."
This is called a trait bound. It BOUNDS (limits) what types T can be.
Full Example
trait MakesSound {
fn sound(&self) -> String;
}
struct Dog {
name: String,
}
impl MakesSound for Dog {
fn sound(&self) -> String {
String::from("Woof!")
}
}
struct Cat {
name: String,
}
impl MakesSound for Cat {
fn sound(&self) -> String {
String::from("Meow!")
}
}
struct Rock {} // Rock doesn't implement MakesSound!
// This function accepts ANY type that implements MakesSound
fn print_sound<T: MakesSound>(item: &T) {
println!("Sound: {}", item.sound());
}
fn main() {
let dog = Dog { name: String::from("Buddy") };
let cat = Cat { name: String::from("Whiskers") };
let rock = Rock {};
print_sound(&dog); // Works! Dog implements MakesSound
print_sound(&cat); // Works! Cat implements MakesSound
// print_sound(&rock); // ERROR! Rock doesn't implement MakesSound
}
The Error You'd Get
If you tried print_sound(&rock):
error: the trait bound `Rock: MakesSound` is not satisfied
Rust catches this at COMPILE TIME. You can't even run the program with this bug.
Step 7: Alternative Syntax: impl Trait
There's a shorter way to write trait bounds for function parameters.
The Long Way (Trait Bound)
fn print_sound<T: MakesSound>(item: &T) {
println!("Sound: {}", item.sound());
}
The Short Way (impl Trait)
fn print_sound(item: &impl MakesSound) {
println!("Sound: {}", item.sound());
}
Breaking Down impl Trait
fn print_sound(item: &impl MakesSound)
// ^^^^
// Keyword
fn print_sound(item: &impl MakesSound)
// ^^^^^^^^^^
// The trait it must implement
Both Are Equivalent
These two are the same:
// Long way
fn print_sound<T: MakesSound>(item: &T) { ... }
// Short way
fn print_sound(item: &impl MakesSound) { ... }
When to Use Which?
Use impl Trait (short way) when:
- You have a simple function with one or two parameters
- You want cleaner, more readable code
Use trait bounds (long way) when:
- You need the same type in multiple places
- You have complex bounds (we'll see this soon)
Example: When You Need the Long Way
// This ensures BOTH items are the SAME type
fn compare_sounds<T: MakesSound>(item1: &T, item2: &T) {
println!("Item 1: {}", item1.sound());
println!("Item 2: {}", item2.sound());
}
With impl Trait, you CAN'T guarantee they're the same type:
// These could be DIFFERENT types (both just need MakesSound)
fn compare_sounds(item1: &impl MakesSound, item2: &impl MakesSound) {
println!("Item 1: {}", item1.sound());
println!("Item 2: {}", item2.sound());
}
Step 8: Multiple Trait Bounds
What if you need a type that implements MORE than one trait?
The Scenario
We want a function that:
- Prints the sound something makes (
MakesSound) - Prints how many legs it has (
HasLegs)
The type needs BOTH traits.
Using + to Combine Traits
fn describe<T: MakesSound + HasLegs>(item: &T) {
println!("Sound: {}", item.sound());
println!("Legs: {}", item.leg_count());
}
Breaking It Down
fn describe<T: MakesSound + HasLegs>(item: &T)
// ^^^^^^^^^^
// Must implement MakesSound
fn describe<T: MakesSound + HasLegs>(item: &T)
// ^
// AND (the + sign)
fn describe<T: MakesSound + HasLegs>(item: &T)
// ^^^^^^^
// Must ALSO implement HasLegs
Full Example
trait MakesSound {
fn sound(&self) -> String;
}
trait HasLegs {
fn leg_count(&self) -> u32;
}
struct Dog {
name: String,
}
impl MakesSound for Dog {
fn sound(&self) -> String {
String::from("Woof!")
}
}
impl HasLegs for Dog {
fn leg_count(&self) -> u32 {
4
}
}
struct Car {
model: String,
}
impl MakesSound for Car {
fn sound(&self) -> String {
String::from("Vroom!")
}
}
// Note: Car does NOT implement HasLegs!
fn describe<T: MakesSound + HasLegs>(item: &T) {
println!("Sound: {}", item.sound());
println!("Legs: {}", item.leg_count());
}
fn main() {
let dog = Dog { name: String::from("Buddy") };
let car = Car { model: String::from("Toyota") };
describe(&dog); // Works! Dog has BOTH traits
// describe(&car); // ERROR! Car only has MakesSound, not HasLegs
}
You Can Require Many Traits
fn do_stuff<T: TraitA + TraitB + TraitC + TraitD>(item: &T) {
// T must implement ALL of these traits
}
Step 9: The where Clause: Cleaner Syntax
When you have many trait bounds, the function signature gets messy:
fn some_function<T: MakesSound + HasLegs, U: MakesSound + Clone>(t: &T, u: &U) -> i32 {
// Hard to read!
}
The where clause makes this cleaner.
Using where
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: MakesSound + HasLegs,
U: MakesSound + Clone,
{
// Much cleaner!
}
Breaking It Down
fn some_function<T, U>(t: &T, u: &U) -> i32
// ^^^^^^
// Just declare the type parameters, no bounds here
fn some_function<T, U>(t: &T, u: &U) -> i32
where
//^^^
//Keyword that starts the bounds section
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: MakesSound + HasLegs,
// ^^^^^^^^^^^^^^^^^^^^^^^
// Bounds for T
U: MakesSound + Clone,
// ^^^^^^^^^^^^^^^^^^^^^
// Bounds for U
{
// function body
}
When to Use where
- When you have multiple type parameters
- When each parameter has multiple bounds
- When you want the code to be more readable
Both styles work the same. where is just for readability.
Step 10: Default Implementations
Sometimes you want a trait method that has a default behavior.
The Problem
Every type that implements a trait must provide ALL the methods. But sometimes you want a sensible default that most types can use.
Example: A Trait With a Default
trait Describable {
fn describe(&self) -> String {
String::from("This is something.")
}
}
Breaking It Down
trait Describable {
fn describe(&self) -> String {
// ^
// Note: NO semicolon!
String::from("This is something.")
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// This is the DEFAULT implementation
}
}
When a method has a body (not just a semicolon), that's the DEFAULT implementation.
Using the Default
trait Describable {
fn describe(&self) -> String {
String::from("This is something.")
}
}
struct Mystery {}
// Implement the trait but DON'T provide describe
impl Describable for Mystery {}
// ^^
// Empty impl block!
fn main() {
let m = Mystery {};
println!("{}", m.describe()); // "This is something."
// Uses the default!
}
Overriding the Default
Types can still provide their own implementation:
trait Describable {
fn describe(&self) -> String {
String::from("This is something.")
}
}
struct Dog {
name: String,
}
// Override the default
impl Describable for Dog {
fn describe(&self) -> String {
format!("A dog named {}", self.name)
}
}
struct Mystery {}
// Use the default
impl Describable for Mystery {}
fn main() {
let dog = Dog { name: String::from("Buddy") };
let mystery = Mystery {};
println!("{}", dog.describe()); // "A dog named Buddy" (custom)
println!("{}", mystery.describe()); // "This is something." (default)
}
Step 11: Default Methods Can Call Other Trait Methods
A default method can call other methods from the same trait, even if those methods don't have defaults!
Example
trait Describable {
// This method has NO default, types MUST implement it
fn name(&self) -> String;
// This method HAS a default, it calls name()
fn describe(&self) -> String {
format!("This is called: {}", self.name())
// ^^^^^^^^^^^
// Calls the other trait method!
}
}
Using It
trait Describable {
fn name(&self) -> String; // Required
fn describe(&self) -> String { // Has default
format!("This is called: {}", self.name())
}
}
struct Dog {
dog_name: String,
}
impl Describable for Dog {
// We MUST implement name() because it has no default
fn name(&self) -> String {
self.dog_name.clone()
}
// We DON'T have to implement describe(), we get it for free!
}
fn main() {
let dog = Dog { dog_name: String::from("Buddy") };
println!("{}", dog.name()); // "Buddy"
println!("{}", dog.describe()); // "This is called: Buddy"
}
Key Point
name()has no default → types MUST provide itdescribe()has a default that usesname()→ types get it for FREE- Types can still override
describe()if they want
Step 12: Returning Types That Implement a Trait
You can use impl Trait in return position too!
Example
trait MakesSound {
fn sound(&self) -> String;
}
struct Dog {
name: String,
}
impl MakesSound for Dog {
fn sound(&self) -> String {
String::from("Woof!")
}
}
fn create_noisy_thing() -> impl MakesSound {
// ^^^^^^^^^^^^^^^
// Returns SOMETHING that implements MakesSound
Dog { name: String::from("Buddy") }
}
Breaking It Down
fn create_noisy_thing() -> impl MakesSound {
// ^^^^
// Keyword
fn create_noisy_thing() -> impl MakesSound {
// ^^^^^^^^^^
// The trait it implements
What This Means
The function says:
"I return something that implements
MakesSound. You can callsound()on it. But I'm not telling you the exact type."
Using It
fn main() {
let thing = create_noisy_thing();
println!("{}", thing.sound()); // "Woof!"
// We can use any MakesSound methods
// But we can't use Dog-specific methods (we don't know it's a Dog)
}
Important Limitation!
You can only return ONE concrete type. This does NOT work:
fn create_noisy_thing(use_dog: bool) -> impl MakesSound {
if use_dog {
Dog { name: String::from("Buddy") }
} else {
Cat { name: String::from("Whiskers") } // ERROR!
}
}
Even though both Dog and Cat implement MakesSound, Rust needs to know the EXACT type at compile time. The function can only return ONE type.
Step 13: Conditionally Implementing Methods
You can implement methods on a generic struct ONLY when the type parameter has certain traits.
Example
struct Wrapper<T> {
value: T,
}
// Methods for ALL Wrapper<T>
impl<T> Wrapper<T> {
fn new(value: T) -> Wrapper<T> {
Wrapper { value }
}
}
// Methods ONLY when T implements MakesSound
impl<T: MakesSound> Wrapper<T> {
fn play_sound(&self) {
println!("Sound: {}", self.value.sound());
}
}
Breaking Down the Conditional impl
impl<T: MakesSound> Wrapper<T> {
// ^^^^^^^^^^^^^^
// T must implement MakesSound
This means play_sound only exists when T implements MakesSound.
Full Example
trait MakesSound {
fn sound(&self) -> String;
}
struct Dog {
name: String,
}
impl MakesSound for Dog {
fn sound(&self) -> String {
String::from("Woof!")
}
}
struct Rock {} // No MakesSound for Rock!
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T> {
fn new(value: T) -> Wrapper<T> {
Wrapper { value }
}
}
impl<T: MakesSound> Wrapper<T> {
fn play_sound(&self) {
println!("Sound: {}", self.value.sound());
}
}
fn main() {
let dog_wrapper = Wrapper::new(Dog { name: String::from("Buddy") });
let rock_wrapper = Wrapper::new(Rock {});
dog_wrapper.play_sound(); // Works! Dog implements MakesSound
// rock_wrapper.play_sound(); // ERROR! Rock doesn't implement MakesSound
}
Key Point
new()exists for ALLWrapper<T>, any type worksplay_sound()ONLY exists whenT: MakesSound
The method just doesn't exist for types that don't have the trait. Compile-time safety!
Step 14: Solving the Original Problem!
Remember where we started? The largest function that wouldn't compile?
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { // ERROR: can't compare T
largest = item;
}
}
largest
}
Now we can fix it with trait bounds!
The Fix
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
What's PartialOrd?
PartialOrd is a trait from Rust's standard library. It means "this type can be compared with <, >, <=, >=."
Types that implement PartialOrd:
i32,i64, all integer types ✓f32,f64(floating point) ✓char✓String,&str✓
Types that DON'T implement PartialOrd:
- Your custom structs (unless you implement it)
- Types where comparison doesn't make sense
Using Our Fixed Function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = [1, 5, 3, 9, 2];
let biggest = largest(&numbers);
println!("Largest number: {}", biggest); // 9
let chars = ['a', 'z', 'm', 'b'];
let biggest_char = largest(&chars);
println!("Largest char: {}", biggest_char); // z
}
It works! The function accepts any type that can be compared.
Step 15: Common Traits from the Standard Library
Rust has many built-in traits. Here are some you'll see often:
PartialOrd: Can be compared with <, >, <=, >=
fn find_max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
Clone: Can be cloned (duplicated)
fn duplicate<T: Clone>(item: &T) -> T {
item.clone()
}
Copy: Can be copied implicitly (like integers)
fn double<T: Copy>(item: T) -> (T, T) {
(item, item) // Uses item twice, needs Copy
}
Debug: Can be printed with {:?}
fn print_debug<T: std::fmt::Debug>(item: &T) {
println!("{:?}", item);
}
Display: Can be printed with {}
fn print_nice<T: std::fmt::Display>(item: &T) {
println!("{}", item);
}
Combining Them
fn do_stuff<T: Clone + PartialOrd + Debug>(item: &T) {
let copy = item.clone();
println!("Item: {:?}", item);
// Can also compare with < >
}
Step 16: Quick Reference
Define a Trait
trait TraitName {
fn required_method(&self) -> ReturnType;
fn default_method(&self) -> ReturnType {
// default implementation
}
}
Implement a Trait
impl TraitName for MyType {
fn required_method(&self) -> ReturnType {
// implementation
}
}
Trait Bound (Long Way)
fn function<T: TraitName>(item: &T) { }
Trait Bound (Short Way)
fn function(item: &impl TraitName) { }
Multiple Bounds
fn function<T: TraitA + TraitB>(item: &T) { }
Where Clause
fn function<T, U>(t: &T, u: &U)
where
T: TraitA + TraitB,
U: TraitC,
{ }
Return impl Trait
fn function() -> impl TraitName { }
Conditional Implementation
impl<T: SomeTrait> MyStruct<T> {
fn special_method(&self) { }
}
Summary: What We Learned About Traits
Traits define capabilities: what a type can DO
Syntax:
trait Name { fn method(&self); }Implement with:
impl Trait for Type { ... }Different types, same trait: many types can share a capability
Trait bounds limit generics:
<T: Trait>means "T must have this capability"Two syntax options:
- Long:
fn foo<T: Trait>(x: &T) - Short:
fn foo(x: &impl Trait)
- Long:
Multiple bounds with
+:T: TraitA + TraitBwhereclause: cleaner syntax for complex boundsDefault implementations: provide fallback behavior
Return
impl Trait: return something that has a capabilityConditional methods: methods that only exist when bounds are met
Standard library traits:
Clone,Copy,PartialOrd,Debug, etc.
Traits Exercises
Exercise 1: Define a Trait
Define a trait called Speak with one method:
trait Speak {
fn speak(&self) -> String;
}
Create a struct Dog (no fields needed).
Implement Speak for Dog so it returns "Woof!".
Test:
let d = Dog {};
println!("{}", d.speak());
Expected output:
Woof!
Exercise 2: Same Trait, Different Types
Using Speak from Exercise 1, add:
- A struct
Cat - Implement
SpeakforCatreturning"Meow!"
Test:
let d = Dog {};
let c = Cat {};
println!("{}", d.speak());
println!("{}", c.speak());
Expected output:
Woof!
Meow!
Exercise 3: Trait with Data
Create a struct Person with a name: String field.
Implement Speak for Person returning "Hi, I'm <name>!".
Test:
let p = Person { name: String::from("Alice") };
println!("{}", p.speak());
Expected output:
Hi, I'm Alice!
Exercise 4: Function with Trait Bound
Write this function:
fn make_speak<T: Speak>(thing: &T) {
println!("Says: {}", thing.speak());
}
Call it with a Dog, Cat, and Person.
Expected output:
Says: Woof!
Says: Meow!
Says: Hi, I'm Alice!
Exercise 5: Alternative Syntax
Rewrite Exercise 4's function using impl Trait:
fn make_speak(thing: &impl Speak) {
println!("Says: {}", thing.speak());
}
Verify it works the same way.
Exercise 6: Default Implementation
Create a new trait with a default method:
trait Greet {
fn greet(&self) -> String {
String::from("Hello!")
}
}
Create two structs:
Friendly: uses the defaultShy: overrides to return"...hi"
Test:
let f = Friendly {};
let s = Shy {};
println!("{}", f.greet());
println!("{}", s.greet());
Expected output:
Hello!
...hi
Hint: To use default, just write impl Greet for Friendly {}
Exercise 7: Two Traits
Define two simple traits:
trait CanWalk {
fn walk(&self) -> String;
}
trait CanSwim {
fn swim(&self) -> String;
}
Create a struct Duck.
Implement BOTH traits for Duck:
walkreturns"Duck walking"swimreturns"Duck swimming"
Test:
let d = Duck {};
println!("{}", d.walk());
println!("{}", d.swim());
Expected output:
Duck walking
Duck swimming
Exercise 8: Multiple Trait Bounds
Write a function that needs BOTH traits from Exercise 7:
fn do_both<T: CanWalk + CanSwim>(thing: &T) {
println!("{}", thing.walk());
println!("{}", thing.swim());
}
Call it with Duck.
Expected output:
Duck walking
Duck swimming
Exercise 9: Using Standard Library Trait
Rust has a built-in trait PartialOrd for comparing with >.
Write this function:
fn is_bigger<T: PartialOrd>(a: T, b: T) -> bool {
a > b
}
Test:
println!("{}", is_bigger(10, 5));
println!("{}", is_bigger(3, 8));
Expected output:
true
false
Exercise 10: Trait Bound on Struct Method
Create:
struct Holder<T> {
value: T,
}
impl<T> Holder<T> {
fn new(value: T) -> Holder<T> {
Holder { value }
}
}
Add a show method that only works when T can be printed:
impl<T: std::fmt::Display> Holder<T> {
fn show(&self) {
println!("{}", self.value);
}
}
Test:
let h = Holder::new(42);
h.show();
Expected output:
42