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



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:

100%
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:

RUST
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 charge method—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:

RUST
// 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

RUST
// 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:

TEXT 📖 Display only
Phone: charging via USB-C at 18W
Laptop: charging via USB-C at 65W
Tablet: charging via USB-C at 30W

trait UsbCCharge defines a "charging behavior contract." Any type that implements this trait can be accepted by charge_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

100%
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:

TEXT 📖 Display only
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)>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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 UsbCCharge trait, charge() is a required method—every implementer must provide it. voltage() and charge_time() are default methods—they provide a default implementation, and implementers can choose to override them or use them as-is. Phone implements only charge() and uses the default implementations for the other two methods; Laptop overrides voltage() and uses the defaults for the rest.


▶ Example 2: Deriving Traits—Debugging, Cloning, and Comparing (Difficulty ⭐⭐)

Output:

TEXT 📖 Display only
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>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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 traits ChargerSpec. Note the difference between Copy and Clone: Clone requires an explicit call to .clone(), while Copy is an implicit bitwise copy (the assignment does not involve a move). Device does not have Copy because the String type does not implement Copy (it allocates memory on the heap).


▶ Example 3: Traits as Parameters—impl Trait and Generic Constraints (Difficulty ⭐⭐)

Output:

TEXT 📖 Display only
[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)>

RUST
// ============================================
// 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:

TEXT 📖 Display only
[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 that Vec<&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:

TEXT 📖 Display only
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()>
RUST
// ============================================
// 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:

TEXT 📖 Display only
--- 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: Debug means "any type that implements AnyDevice must also implement Debug." trait UsbDevice: AnyDevice is further layered on top—forming a three-tier trait hierarchy. Types that implement UsbDevice must also implement all methods of Debug + AnyDevice + UsbDevice.

Trait Object dyn Trait: Using Box<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. The make_device function, which returns Option<Box<dyn UsbDevice>>, embodies the "Factory Pattern."


▶ Example 5: Comprehensive Practice—Calculating the Area of Shapes (Difficulty ⭐⭐⭐)

Output:

TEXT 📖 Display only
  <s.describe()>
=== Static Reference Traversal ===
  <s.describe()>

=== Dynamic Distribution ===
Total Area: <total_area(&shapes_dynamic)>

RUST
// ============================================
// 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:

TEXT 📖 Display only
=== 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 Shape trait is used in three ways: &dyn Shape for static slice references, Box<dyn Shape> for dynamic collection distribution, and T: Shape for finding the maximum value as a generic constraint. describe is the trait’s default method, which all implementers automatically inherit.


❓ FAQ

Q What is the difference between impl Trait and dyn Trait? When should each be used?
A impl Trait uses static dispatch at compile time, while dyn Trait uses dynamic dispatch at runtime.
Q What is the difference between #[derive(Debug)] and manual implementation?
A derive automatically generates boilerplate code, which is suitable for simple structs with few fields.
Q What exactly is the difference between Copy and Clone?
A Copy is an implicit bitwise copy (assignment does not transfer ownership), while Clone is an explicit deep copy (calls .clone()).
Q What is the difference between trait inheritance and class inheritance in object-oriented languages?
A Trait inheritance in Rust is "interface inheritance" (inheriting method signatures), not "implementation inheritance" (inheriting implementations + state).
Q Are the T: Trait1 + Trait2 and where T: Trait1 + Trait2 notations in trait constraints the same?
A They have exactly the same semantics; they just differ in syntax style.
Q What is the purpose of an empty trait (marker trait)?
A A marker trait has no methods; it is used to "label" a type in order to enable certain compiler behaviors or constraints.

📖 Summary


📝 Exercises

  1. Difficulty ⭐: Define a trait Drawable { fn draw(&self); }. Implement this trait for the two structs Circle and Square, with each printing a different shape. Write a generic function fn render<T: Drawable>(item: &T) that calls draw. In the main function, render a circle and a square, respectively.

  2. Difficulty ⭐⭐: Define a trait Summary { fn summarize(&self) -> String; fn author(&self) -> &str; } trait, where author() is the default method that returns "Anonymous". Implement this trait for the structs Article { title: String, content: String } and Tweet { username: String, text: String }. Create two articles and two tweets, place them in Vec<Box<dyn Summary>>, and iterate through them to print their summaries.

  3. Difficulty ⭐⭐⭐: Define a trait hierarchy: trait Vehicle: std::fmt::Debug { fn fuel_type(&self) -> &str; }, then trait ElectricVehicle: Vehicle { fn battery_capacity_kwh(&self) -> f64; fn range_km(&self) -> f64; }. Implement ElectricVehicle for the structs TeslaModel3 and NissanLeaf. Write a function fn 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.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏