Rust: Rust Traits: Defining Shared Behavior
Last updated: 2026-08-26
A trait is Rust's equivalent of an "interface"—it defines a set of method signatures, and different types can implement the same trait, thereby sharing the same behavioral contract.
If a concrete type is “what this thing is,” then a trait is “what this thing can do.” Take the USB-C port, for example—whether it’s a phone, laptop, or tablet, as long as it supports USB-C, you can plug it in to charge. The internal workings of these devices may vary greatly, but the behavior they promise to users is consistent.
1. What You'll Learn
- Syntax for defining a trait
trait Name { fn method(&self); }and implementing a trait for a type - Default method—Provide a default implementation in the trait definition; implementers can choose to override it
- Derivable Trait:
#[derive(Debug, Clone, Copy, PartialEq)] - Traits as parameters:
impl TraitSyntax and generic constraintsT: Trait - trait object
dyn Trait—Dynamic distribution at runtime - Trait inheritance (super trait): A trait inherits methods from another trait
2. Conceptual Diagrams
The following Mermaid diagram illustrates the complete chain of relationships for a trait—from its definition to its implementation for various types, and finally to its polymorphic invocation:
graph LR
A["trait UsbCCharge"] -->|Defining a Behavioral Contract| B["fn charge(&self)"]
A --> C["fn voltage(&self) -> u32"]
A --> D["fn charge_time(&self) -> String"]
B --> E["impl for Phone<br/>charge: 18W"]
B --> F["impl for Laptop<br/>charge: 65W"]
B --> G["impl for Tablet<br/>charge: 30W"]
E --> H["phone.charge()"]
F --> I["laptop.charge()"]
G --> J["tablet.charge()"]
H --> K["Polymorphic Call<br/>The Same trait Interface<br/>Different types of behavior vary"]
I --> K
J --> K
3. The Story of the Universal Charger
(1) The hassle: Every device has its own charging method
Leo (Leo) is an architect at an electronics company. The company’s product line is expanding to include phones, laptops, and tablets, and each device has its own charging logic.
At first, he wrote charging code separately for each device:
struct Phone;
struct Laptop;
struct Tablet;
impl Phone {
fn charge(&self) {
println!("Phone: charging via USB-C at 18W");
}
}
impl Laptop {
fn charge(&self) {
println!("Laptop: charging via USB-C at 65W");
}
}
impl Tablet {
fn charge(&self) {
println!("Tablet: charging via USB-C at 30W");
}
}
fn main() {
let phone = Phone;
let laptop = Laptop;
let tablet = Tablet;
phone.charge();
laptop.charge();
tablet.charge();
}
Each of the three structures has a
chargemethod—they share the same name and signature, but there is no "agreement" between them. If you wanted to write a "universal charging station" function to charge any device—it wouldn’t be possible. Each device type is independent, with no common abstract layer.
(2) More Complex Requirements: Batch Charging
The product manager has requested the development of a "universal charging station" capable of charging multiple different types of devices simultaneously:
// This code cannot be compiled.——Unknown parameter type
// fn charge_all_devices(devices: ???) {
// for device in devices {
// device.charge();
// }
// }
Without a trait, charge_all_devices cannot accept collections of mixed types. Either overload a function for each type, or give up.
(3) The Rust trait approach
// Define a trait: a shared behavior contract
trait UsbCCharge {
fn charge(&self);
fn voltage(&self) -> u32 { 18 } // Default method with default voltage
}
struct Phone;
struct Laptop;
struct Tablet;
impl UsbCCharge for Phone {
fn charge(&self) {
println!("Phone: charging via USB-C at {}W", self.voltage());
}
// voltage() uses the default (18W)
}
impl UsbCCharge for Laptop {
fn charge(&self) {
println!("Laptop: charging via USB-C at {}W", self.voltage());
}
fn voltage(&self) -> u32 { 65 } // Override default
}
impl UsbCCharge for Tablet {
fn charge(&self) {
println!("Tablet: charging via USB-C at {}W", self.voltage());
}
fn voltage(&self) -> u32 { 30 } // Override default
}
// Generic function: accepts any type that implements UsbCCharge
fn charge_device<T: UsbCCharge>(device: &T) {
device.charge();
}
fn main() {
charge_device(&Phone);
charge_device(&Laptop);
charge_device(&Tablet);
}
Output:
Phone: charging via USB-C at 18W
Laptop: charging via USB-C at 65W
Tablet: charging via USB-C at 30W
trait UsbCChargedefines a "charging behavior contract." Any type that implements this trait can be accepted bycharge_device.voltage()has a default implementation (18W), but Laptop and Tablet choose to override it. This is the core value of a trait—defining shared behavior while allowing for differentiated implementations.
4. Core Concepts
(1) Overview of the Trait System
graph TB
A[Rust Trait System] --> B[Trait Definition]
A --> C[Implementing Traits]
A --> D[Derivable Traits]
A --> E[Trait as Parameters]
A --> F[Trait Objects]
A --> G[Trait Inheritance]
B --> B1["trait Name { fn method(&self); }"]
C --> C1["impl TraitName for MyType { ... }"]
C --> C2["Default methods in trait"]
D --> D1["#[derive(Debug, Clone, Copy, PartialEq)]"]
D --> D2["Compiler auto-generates implementation"]
E --> E1["fn foo(x: impl Trait)"]
E --> E2["fn foo<T: Trait>(x: T)"]
E --> E3["fn foo<T>(x: T) where T: Trait"]
F --> F1["Box<dyn Trait>"]
F --> F2["Runtime dispatch (vtable)"]
G --> G1["trait A: SuperTrait { }"]
G --> G2["Inherits methods from SuperTrait"]
(2) Static Distribution vs. Dynamic Distribution
| Feature | Generic Constraints T: Trait / impl Trait |
Trait Instances dyn Trait |
|---|---|---|
| Distribution Timing | Compile Time (Static Distribution) | Runtime (Dynamic Distribution) |
| Implementation | Singleton—Generate separate code for each type | Virtual table (vtable)—Indirect call via pointers |
| Performance | Zero overhead (can be inlined) | Indirect call overhead |
| Binary Size | Larger (one copy per type) | Smaller (one copy of the code) |
| Type Requirements | The specific type is determined at call time | Types may differ, as long as they implement the same trait |
| Use Cases | Performance-sensitive, with types known at compile time | Requires heterogeneous collections, with types determined at runtime |
(3) Commonly Used Derivable Traits
| Trait | Function | Automatically Generated Behavior |
|---|---|---|
Debug |
Formatted output {:?} |
Generate debug output; print struct field names and values |
Clone |
Explicitly copy .clone() |
Generate clone method, copying field by field |
Copy |
Implicit copying (bit-by-bit copy) | Ownership is not transferred during assignment; instead, the value is copied |
PartialEq |
Equality Comparison == / != |
Generate eq Method, Compare Field by Field |
Eq |
Total equivalence (mathematical equivalence relation) | Based on PartialEq, with the additional guarantee of reflexivity |
Hash |
Hash Calculation | Method to generate hash, calculating hash values field by field |
Default |
Default value | Generate the default method, using the default value for each field |
(4) Quick Reference for Combining Trait Constraints
| Constraint Type | Syntax | Use Cases | Example |
|---|---|---|---|
| Inline Single Constraint | fn foo<T: Trait>(x: T) |
Simple Single Constraint | fn charge<T: UsbCCharge>(d: &T) |
| Inline Multiple Constraints | fn foo<T: Trait1 + Trait2>(x: T) |
Multiple Constraints | fn describe<T: Debug + UsbCCharge>(d: &T) |
| WHERE clause | fn foo<T>(x: T) where T: Trait |
Complex constraints, multi-type parameters | where T: UsbCCharge, U: Debug |
| impl Trait | fn foo(x: impl Trait) |
Simple Syntax Sugar | fn plug(d: &impl USBDevice) |
| impl Trait + | fn foo(x: impl Trait1 + Trait2) |
Simple and Constrained | fn describe(d: &(impl USBDevice + Debug)) |
5. Stroke Examples
▶ Example 1: Trait Definitions and Default Methods—Device Charger (Difficulty ⭐)
Output:
Phone: charging at <self.voltage()>W (standard speed)
Laptop: charging at <self.voltage()>W (fast charging)
Tablet: charging at <self.voltage()>W (medium speed)
-> <phone.charge_time(3000)>
-> <laptop.charge_time(6000)>
-> <tablet.charge_time(5000)>
// ============================================
// Trait definition with default methods
// ============================================
// Define a trait: any device that can charge via USB-C
trait UsbCCharge {
// Required method: must be implemented
fn charge(&self);
// Default method: implementor MAY override
fn voltage(&self) -> u32 {
18 // Default: standard USB-C 18W
}
// Another default method using self.voltage()
fn charge_time(&self, battery_capacity_mah: u32) -> String {
let hours = battery_capacity_mah as f64 / (self.voltage() as f64 * 1000.0 / 5.0);
format!("{:.1} hours to full charge", hours)
}
}
struct Phone;
struct Laptop;
struct Tablet;
impl UsbCCharge for Phone {
fn charge(&self) {
// Uses the default voltage() -> 18W
println!("Phone: charging at {}W (standard speed)", self.voltage());
}
// voltage() uses default, charge_time() uses default
}
impl UsbCCharge for Laptop {
fn charge(&self) {
println!("Laptop: charging at {}W (fast charging)", self.voltage());
}
fn voltage(&self) -> u32 {
65 // Laptop needs more power
}
}
impl UsbCCharge for Tablet {
fn charge(&self) {
println!("Tablet: charging at {}W (medium speed)", self.voltage());
}
fn voltage(&self) -> u32 {
30
}
}
fn main() {
let phone = Phone;
let laptop = Laptop;
let tablet = Tablet;
phone.charge();
println!(" -> {}", phone.charge_time(3000));
laptop.charge();
println!(" -> {}", laptop.charge_time(6000));
tablet.charge();
println!(" -> {}", tablet.charge_time(5000));
}
Output:
Phone: charging at 18W (standard speed)
-> 0.8 hours to full charge
Laptop: charging at 65W (fast charging)
-> 0.5 hours to full charge
Tablet: charging at 30W (medium speed)
-> 0.8 hours to full charge
In the
UsbCChargetrait,charge()is a required method—every implementer must provide it.voltage()andcharge_time()are default methods—they provide a default implementation, and implementers can choose to override them or use them as-is. Phone implements onlycharge()and uses the default implementations for the other two methods; Laptop overridesvoltage()and uses the defaults for the rest.
▶ Example 2: Deriving Traits—Debugging, Cloning, and Comparing (Difficulty ⭐⭐)
Output:
Debug: <spec1>
Pretty: <spec1>
Cloned: <spec2>
Copied (implicit): <spec3>
Original still valid: <spec1>
spec_a == spec_b: <spec_a == spec_b>
spec_a == spec_c: <spec_a == spec_c>
spec_a != spec_c: <spec_a != spec_c>
Phone backup: <phone_backup>
// ============================================
// Derivable traits: Debug, Clone, Copy, PartialEq
// ============================================
// Without deriving, none of these operations would work
#[derive(Debug, Clone, Copy, PartialEq)]
struct ChargerSpec {
brand: &'static str,
watts: u32,
usb_c: bool,
}
// PartialEq is needed for this custom type
#[derive(Debug, Clone, PartialEq)]
struct Device {
name: String,
required_watts: u32,
}
fn main() {
// --- Debug: pretty-print with {:?} ---
let spec1 = ChargerSpec { brand: "Anker", watts: 65, usb_c: true };
println!("Debug: {:?}", spec1);
println!("Pretty: {:#?}", spec1);
// --- Clone: explicit copy ---
let spec2 = spec1.clone(); // spec1 is still valid
println!("Cloned: {:?}", spec2);
// --- Copy: implicit copy (only if Copy is derived) ---
let spec3 = spec1; // spec1 is STILL valid because ChargerSpec is Copy!
println!("Copied (implicit): {:?}", spec3);
println!("Original still valid: {:?}", spec1); // Works!
// --- PartialEq: equality comparison ---
let spec_a = ChargerSpec { brand: "Anker", watts: 65, usb_c: true };
let spec_b = ChargerSpec { brand: "Anker", watts: 65, usb_c: true };
let spec_c = ChargerSpec { brand: "Baseus", watts: 65, usb_c: true };
println!("spec_a == spec_b: {}", spec_a == spec_b); // true
println!("spec_a == spec_c: {}", spec_a == spec_c); // false (brand differs)
println!("spec_a != spec_c: {}", spec_a != spec_c); // true
// --- Practical: filtering devices ---
let phone = Device {
name: String::from("Phone"),
required_watts: 18,
};
let laptop = Device {
name: String::from("Laptop"),
required_watts: 65,
};
// Clone a device
let phone_backup = phone.clone();
println!("\nPhone backup: {:?}", phone_backup);
// Compare devices by required_watts
let charger_watts = 65;
let compatible = vec![phone, laptop]
.iter()
.filter(|d| d.required_watts <= charger_watts)
.collect::<Vec<_>>();
println!("Compatible devices (<= {}W): {:?}", charger_watts, compatible);
}
Output:
Debug: ChargerSpec { brand: "Anker", watts: 65, usb_c: true }
Pretty: ChargerSpec {
brand: "Anker",
watts: 65,
usb_c: true,
}
Cloned: ChargerSpec { brand: "Anker", watts: 65, usb_c: true }
Copied (implicit): ChargerSpec { brand: "Anker", watts: 65, usb_c: true }
Original still valid: ChargerSpec { brand: "Anker", watts: 65, usb_c: true }
spec_a == spec_b: true
spec_a == spec_c: false
spec_a != spec_c: true
Phone backup: Device { name: "Phone", required_watts: 18 }
Compatible devices (<= 65W): [Device { name: "Phone", required_watts: 18 }, Device { name: "Laptop", required_watts: 65 }]
With just one line of code
#[derive(Debug, Clone, Copy, PartialEq)], the Rust compiler automatically generated implementations for four traitsChargerSpec. Note the difference betweenCopyandClone:Clonerequires an explicit call to.clone(), whileCopyis an implicit bitwise copy (the assignment does not involve a move).Devicedoes not haveCopybecause theStringtype does not implementCopy(it allocates memory on the heap).
▶ Example 3: Traits as Parameters—impl Trait and Generic Constraints (Difficulty ⭐⭐)
Output:
[Plugged] <device.device_name()> (draws <device.power_draw()>W)
[Spec] <device.device_name()> - Power: <device.power_draw()>W
[Debug] Device: <device>
--- Compatibility Check (max 3W) ---
Mouse compatible: <check_compatible(&mouse, 3)>
Keyboard compatible: <check_compatible(&keyboard, 3)>
Webcam compatible: <check_compatible(&webcam, 3)>
// ============================================
// Trait as parameter: impl Trait, generic bounds, where clause
// ============================================
trait USBDevice {
fn device_name(&self) -> &str;
fn power_draw(&self) -> u32;
}
struct Mouse;
struct Keyboard;
struct Webcam;
impl USBDevice for Mouse {
fn device_name(&self) -> &str { "Mouse" }
fn power_draw(&self) -> u32 { 2 }
}
impl USBDevice for Keyboard {
fn device_name(&self) -> &str { "Keyboard" }
fn power_draw(&self) -> u32 { 3 }
}
impl USBDevice for Webcam {
fn device_name(&self) -> &str { "Webcam" }
fn power_draw(&self) -> u32 { 5 }
}
// --- Style 1: impl Trait (sugar for simple cases) ---
fn plug_device(device: &impl USBDevice) {
println!("[Plugged] {} (draws {}W)", device.device_name(), device.power_draw());
}
// --- Style 2: Generic bound T: Trait (explicit type parameter) ---
fn print_device_spec<T: USBDevice>(device: &T) {
println!("[Spec] {} - Power: {}W", device.device_name(), device.power_draw());
}
// --- Style 3: where clause (best for complex bounds) ---
fn check_compatible<T>(device: &T, max_power: u32) -> bool
where
T: USBDevice,
{
device.power_draw() <= max_power
}
// --- Style 4: Multiple trait bounds ---
use std::fmt::Debug;
fn describe_device(device: &(impl USBDevice + Debug)) {
println!("[Debug] Device: {:?}", device);
}
fn main() {
let mouse = Mouse;
let keyboard = Keyboard;
let webcam = Webcam;
// impl Trait syntax
plug_device(&mouse);
plug_device(&keyboard);
// Generic bound syntax
print_device_spec(&webcam);
// where clause
println!("\n--- Compatibility Check (max 3W) ---");
println!("Mouse compatible: {}", check_compatible(&mouse, 3));
println!("Keyboard compatible: {}", check_compatible(&keyboard, 3));
println!("Webcam compatible: {}", check_compatible(&webcam, 3));
// Calculate total power draw for a list
let devices: Vec<&dyn USBDevice> = vec![&mouse, &keyboard, &webcam];
let total_power: u32 = devices.iter().map(|d| d.power_draw()).sum();
println!("\nTotal power draw: {}W / 15W budget", total_power);
}
Output:
[Plugged] Mouse (draws 2W)
[Plugged] Keyboard (draws 3W)
[Spec] Webcam - Power: 5W
--- Compatibility Check (max 3W) ---
Mouse compatible: true
Keyboard compatible: true
Webcam compatible: false
Total power draw: 10W / 15W budget
Each of the four trait parameter styles has its own use case:
impl Trait(concise, suitable for a single trait),T: Trait(explicit type parameter names, suitable for reference types),where T: Trait(best readability when there are multiple constraints),impl Trait + AnotherTrait(multiple constraints. Note thatVec<&dyn USBDevice>is a trait object (see the next example)—used here to store references of different types.)
▶ Example 4: The dyn Trait Trait Object and Trait Inheritance (Difficulty: ⭐⭐⭐)
Output:
Device #<device.model_name()>: <device.usb_version()> (<device.transfer_speed()> - <device>, Debug: <i + 1>)
--- Connected Devices ---
--- Device Factory ---
Created: <device.model_name()> (<device.usb_version()> - <device.transfer_speed()>)
--- Debug via Super Trait ---
Debug: <flash>
Model: <flash.model_name()>
USB: <flash.usb_version()>
// ============================================
// dyn Trait (runtime dispatch) + Trait inheritance
// ============================================
use std::fmt::Debug;
// --- Super trait (trait inheritance) ---
// AnyDevice "inherits" from Debug: to implement AnyDevice,
// a type must also implement Debug
trait AnyDevice: Debug {
fn model_name(&self) -> &str;
}
// UsbDevice extends AnyDevice: it requires Debug + AnyDevice
trait UsbDevice: AnyDevice {
fn usb_version(&self) -> &str;
fn transfer_speed(&self) -> &str;
}
// --- Implement the trait hierarchy ---
#[derive(Debug)]
struct FlashDrive {
name: String,
capacity_gb: u32,
}
impl AnyDevice for FlashDrive {
fn model_name(&self) -> &str {
&self.name
}
}
impl UsbDevice for FlashDrive {
fn usb_version(&self) -> &str {
"USB 3.2 Gen 2"
}
fn transfer_speed(&self) -> &str {
"10 Gbps"
}
}
#[derive(Debug)]
struct ExternalSSD {
name: String,
capacity_tb: f64,
}
impl AnyDevice for ExternalSSD {
fn model_name(&self) -> &str {
&self.name
}
}
impl UsbDevice for ExternalSSD {
fn usb_version(&self) -> &str {
"USB 3.2 Gen 2x2"
}
fn transfer_speed(&self) -> &str {
"20 Gbps"
}
}
// --- Function using trait objects ---
// Accept a heterogeneous collection of UsbDevice implementors
fn list_devices(devices: &[Box<dyn UsbDevice>]) {
for (i, device) in devices.iter().enumerate() {
println!(
"Device #{}: {} ({} - {}, Debug: {:?})",
i + 1,
device.model_name(),
device.usb_version(),
device.transfer_speed(),
device,
);
}
}
// --- Function returning a trait object ---
fn make_device(device_type: &str) -> Option<Box<dyn UsbDevice>> {
match device_type {
"flash" => Some(Box::new(FlashDrive {
name: String::from("SanDisk 128GB"),
capacity_gb: 128,
})),
"ssd" => Some(Box::new(ExternalSSD {
name: String::from("Samsung T7 1TB"),
capacity_tb: 1.0,
})),
_ => None,
}
}
fn main() {
// Heterogeneous collection: different types, same trait
let drive1 = Box::new(FlashDrive {
name: String::from("Kingston 64GB"),
capacity_gb: 64,
});
let drive2 = Box::new(ExternalSSD {
name: String::from("WD My Passport 2TB"),
capacity_tb: 2.0,
});
let all_devices: Vec<Box<dyn UsbDevice>> = vec![drive1, drive2];
println!("--- Connected Devices ---");
list_devices(&all_devices);
// Factory function returning trait objects
println!("\n--- Device Factory ---");
if let Some(device) = make_device("ssd") {
println!("Created: {} ({} - {})", device.model_name(), device.usb_version(), device.transfer_speed());
}
// Trait inheritance in action: UsbDevice requires Debug
// so we can use both {:?} and trait methods
println!("\n--- Debug via Super Trait ---");
let flash = FlashDrive {
name: String::from("Lexar 32GB"),
capacity_gb: 32,
};
// flash has Debug (from AnyDevice: Debug), AnyDevice, and UsbDevice
println!("Debug: {:?}", flash);
println!("Model: {}", flash.model_name());
println!("USB: {}", flash.usb_version());
}
Output:
--- Connected Devices ---
Device #1: Kingston 64GB (USB 3.2 Gen 2 - 10 Gbps, Debug: FlashDrive { name: "Kingston 64GB", capacity_gb: 64 })
Device #2: WD My Passport 2TB (USB 3.2 Gen 2x2 - 20 Gbps, Debug: ExternalSSD { name: "WD My Passport 2TB", capacity_tb: 2.0 })
--- Device Factory ---
Created: Samsung T7 1TB (USB 3.2 Gen 2x2 - 20 Gbps)
--- Debug via Super Trait ---
Debug: FlashDrive { name: "Lexar 32GB", capacity_gb: 32 }
Model: Lexar 32GB
USB: USB 3.2 Gen 2
Trait Inheritance (super-trait):
trait AnyDevice: Debugmeans "any type that implementsAnyDevicemust also implementDebug."trait UsbDevice: AnyDeviceis further layered on top—forming a three-tier trait hierarchy. Types that implementUsbDevicemust also implement all methods ofDebug+AnyDevice+UsbDevice.Trait Object
dyn Trait: UsingBox<dyn UsbDevice>, you can store objects of different types that implement the same trait in a single collection. Method calls are dispatched at runtime via the virtual table (vtable)—this incurs a slight performance overhead but offers tremendous flexibility. Themake_devicefunction, which returnsOption<Box<dyn UsbDevice>>, embodies the "Factory Pattern."
▶ Example 5: Comprehensive Practice—Calculating the Area of Shapes (Difficulty ⭐⭐⭐)
Output:
<s.describe()>
=== Static Reference Traversal ===
<s.describe()>
=== Dynamic Distribution ===
Total Area: <total_area(&shapes_dynamic)>
// ============================================
// Comprehensive Example:Trait + Generic Constraints + dyn Trait
// ============================================
use std::fmt::Debug;
trait Shape: Debug {
fn area(&self) -> f64;
fn name(&self) -> &str;
fn describe(&self) -> String {
format!("{}: Area = {:.2}", self.name(), self.area())
}
}
#[derive(Debug)]
struct Circle { radius: f64 }
#[derive(Debug)]
struct Rectangle { width: f64, height: f64 }
#[derive(Debug)]
struct Triangle { base: f64, height: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
fn name(&self) -> &str { "Circular" }
}
impl Shape for Rectangle {
fn area(&self) -> f64 { self.width * self.height }
fn name(&self) -> &str { "Rectangle" }
}
impl Shape for Triangle {
fn area(&self) -> f64 { 0.5 * self.base * self.height }
fn name(&self) -> &str { "Triangle" }
}
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn largest<T: Shape>(shapes: &[T]) -> &T {
shapes.iter().max_by(|a, b| a.area().partial_cmp(&b.area()).unwrap()).unwrap()
}
fn print_all(shapes: &[Box<dyn Shape>]) {
for s in shapes {
println!(" {}", s.describe());
}
}
fn main() {
let shapes_static: Vec<&dyn Shape> = vec![
&Circle { radius: 5.0 },
&Rectangle { width: 4.0, height: 6.0 },
&Triangle { base: 3.0, height: 8.0 },
];
println!("=== Static Reference Traversal ===");
for s in &shapes_static {
println!(" {}", s.describe());
}
let shapes_dynamic: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 10.0 }),
Box::new(Rectangle { width: 3.0, height: 7.0 }),
Box::new(Triangle { base: 6.0, height: 4.0 }),
];
println!("\n=== Dynamic Distribution ===");
print_all(&shapes_dynamic);
println!("Total Area: {:.2}", total_area(&shapes_dynamic));
let homogenous = vec![
Circle { radius: 3.0 },
Circle { radius: 7.0 },
Circle { radius: 5.0 },
];
let biggest = largest(&homogenous);
println!("\nLargest Circle: {}", biggest.describe());
}
Output:
=== Static Reference Traversal ===
Circular: Area = 78.54
Rectangle: Area = 24.00
Triangle: Area = 12.00
=== Dynamic Distribution ===
Circular: Area = 314.16
Rectangle: Area = 21.00
Triangle: Area = 12.00
Total Area: 347.16
Largest Circle: Circular: Area = 153.94
The same
Shapetrait is used in three ways:&dyn Shapefor static slice references,Box<dyn Shape>for dynamic collection distribution, andT: Shapefor finding the maximum value as a generic constraint.describeis the trait’s default method, which all implementers automatically inherit.
❓ FAQ
impl Trait and dyn Trait? When should each be used?impl Trait uses static dispatch at compile time, while dyn Trait uses dynamic dispatch at runtime.#[derive(Debug)] and manual implementation?derive automatically generates boilerplate code, which is suitable for simple structs with few fields.Copy and Clone?Copy is an implicit bitwise copy (assignment does not transfer ownership), while Clone is an explicit deep copy (calls .clone()).T: Trait1 + Trait2 and where T: Trait1 + Trait2 notations in trait constraints the same?📖 Summary
- Trait definition
trait Name { fn method(&self); }declares a set of behavioral contracts; the impl block provides concrete implementations of these contracts for specific types. - Default methods provide default implementations in a trait; implementers can choose to override them or use them as is.
- Derived trait
#[derive(Debug, Clone, Copy, PartialEq)]allows the compiler to automatically generate common trait implementations—abstraction at no cost impl Trait(static distribution) anddyn Trait(dynamic distribution) are two ways to use traits—the former has zero overhead, while the latter offers greater flexibility- Trait Inheritance
trait A: SuperTraitEstablishing a trait hierarchy—implementers must satisfy all constraints of their supertraits simultaneously - Traits are the core mechanism in Rust for implementing zero-cost abstraction—they are "interfaces" (generics) without the overhead of virtual functions, and they also support optional runtime polymorphism when needed (trait objects).
📝 Exercises
-
Difficulty ⭐: Define a
trait Drawable { fn draw(&self); }. Implement this trait for the two structsCircleandSquare, with each printing a different shape. Write a generic functionfn render<T: Drawable>(item: &T)that callsdraw. In the main function, render a circle and a square, respectively. -
Difficulty ⭐⭐: Define a
trait Summary { fn summarize(&self) -> String; fn author(&self) -> &str; }trait, whereauthor()is the default method that returns"Anonymous". Implement this trait for the structsArticle { title: String, content: String }andTweet { username: String, text: String }. Create two articles and two tweets, place them inVec<Box<dyn Summary>>, and iterate through them to print their summaries. -
Difficulty ⭐⭐⭐: Define a trait hierarchy:
trait Vehicle: std::fmt::Debug { fn fuel_type(&self) -> &str; }, thentrait ElectricVehicle: Vehicle { fn battery_capacity_kwh(&self) -> f64; fn range_km(&self) -> f64; }. ImplementElectricVehiclefor the structsTeslaModel3andNissanLeaf. Write a functionfn print_fleet(vehicles: &[Box<dyn ElectricVehicle>])to iterate through and print the information for each car. In the main function, create instances of both car types and test them.