Share Behavior with Traits

RustBeginner
Practice Now

Introduction

Unrelated data types sometimes need to promise the same behavior. A greenhouse sensor and a shipment have different fields, but each can describe its current status. Rust traits name that shared capability without forcing the types to share their data layout.

In this lab, you will define one small trait, implement it for two prepared structures, and pass both values to one generic reporting function. The longer structures and sample data are already created so each edit stays focused.

Define a Shared Status Contract

In this step, you will define a trait that names one behavior shared by otherwise unrelated types.

Move into the prepared package and inspect the complete scaffold. The sed -n command prints the requested lines without editing them:

cd /home/labex/project/status-reporter
sed -n '1,160p' src/main.rs

Sensor and Shipment have different fields. Their prepared text fields use &'static str, meaning borrowed string literals that remain valid for the whole program. The 'static lifetime annotation is not part of this trait lesson and is intentionally deferred; you do not need to edit or reason about it here.

Below the placeholders, show_status is already generic. Read T: Status in three parts: T stands for a concrete type, the colon means “must satisfy,” and Status is the required trait. In plain language: “for any type T, provided that T implements Status.” That promise lets the function call item.status() safely.

The trait must exist before Rust can understand the bound. Open the source:

nano src/main.rs

Replace // STATUS_TRAIT with:

trait Status {
    fn status(&self) -> String;
}

The method line ends with a semicolon because the trait declares a required method but does not choose one implementation. &self lets the method inspect a value without taking ownership, and String allows each type to build its own status text. Each later implementation must repeat this method name, receiver, and return type exactly, then provide its own body.

Save with Ctrl+O, press Enter, and exit with Ctrl+X. Then check the package:

cargo check

You may see warnings that the trait and prepared items are not used yet. A final Finished line is the important evidence: the shared contract and the generic bound are valid. The next two steps will add the implementations and calls.

Implement Status for Sensor

In this step, you will fulfill the trait contract for Sensor and send a sensor through the shared reporter.

An impl Trait for Type block connects an existing trait to one concrete type. Its method signature must match the trait, while its body may use that type’s own fields.

Open the source:

nano src/main.rs

Replace // SENSOR_IMPLEMENTATION with this short implementation:

impl Status for Sensor {
    fn status(&self) -> String {
        format!("{}: {}°C", self.location, self.celsius)
    }
}

format! builds and returns a String; it does not print by itself. The implementation can use location and celsius because self is a borrowed Sensor.

Next, replace // SENSOR_REPORT inside main with:

    show_status("Sensor", &sensor);

The &sensor argument matches the reporter’s borrowed &T parameter, so sensor remains owned by main. Save and exit Nano, then run the program:

cargo run --quiet
Sensor: Greenhouse: 24°C

This line proves that Sensor fulfills Status and can pass through the bounded generic reporter. The shipment has not been connected yet.

Implement Status for Shipment

In this step, you will implement the same trait for Shipment and prove that one reporter accepts both concrete types.

Open the source again:

nano src/main.rs

Replace // SHIPMENT_IMPLEMENTATION with:

impl Status for Shipment {
    fn status(&self) -> String {
        format!("#{} {}", self.id, self.stage)
    }
}

This method follows the same trait contract but uses the shipment’s own id and stage fields. Next, replace // SHIPMENT_REPORT with:

    show_status("Shipment", &shipment);

Save and exit Nano. Run the completed package:

cargo run --quiet
Sensor: Greenhouse: 24°C
Shipment: #204 in transit

Both lines come from show_status<T: Status>. The bound describes the behavior the function needs, while each implementation controls how its own value produces that behavior.

Summary

You defined a shared behavior with a trait, implemented it independently for two structures, and used a basic trait bound so one generic reporter could accept both types.