Home Wiki Programming & Logic Traits and Generics in Rust: Zero-Cost Abstraction for Industrial Code
Programming & Logic

Traits and Generics in Rust: Zero-Cost Abstraction for Industrial Code

Traits: Defining Shared Behavior

A trait defines a set of methods that types can implement. Think of it as a contract: any type that implements a trait guarantees it provides that behavior.

trait Readable {
    fn read_value(&self) -> f64;
    fn unit(&self) -> &str;

    // Default implementation -- types can override this
    fn display_reading(&self) {
        println!("{} {}", self.read_value(), self.unit());
    }
}

Traits are how Rust achieves polymorphism without inheritance.

Implementing Traits for Different Types

Different sensor types can all implement the same Readable trait, each providing its own logic.

struct TemperatureSensor {
    celsius: f64,
}

struct PressureSensor {
    bar: f64,
}

trait Readable {
    fn read_value(&self) -> f64;
    fn unit(&self) -> &str;
}

impl Readable for TemperatureSensor {
    fn read_value(&self) -> f64 {
        self.celsius
    }
    fn unit(&self) -> &str {
        "°C"
    }
}

impl Readable for PressureSensor {
    fn read_value(&self) -> f64 {
        self.bar
    }
    fn unit(&self) -> &str {
        "bar"
    }
}

fn main() {
    let temp = TemperatureSensor { celsius: 85.2 };
    let press = PressureSensor { bar: 4.7 };
    println!("Temperature: {} {}", temp.read_value(), temp.unit());
    println!("Pressure: {} {}", press.read_value(), press.unit());
}

Derived Traits: Debug, Clone, and PartialEq

Rust can automatically generate common trait implementations using #[derive]. This saves boilerplate for standard behaviors.

#[derive(Debug, Clone, PartialEq)]
struct SensorReading {
    sensor_id: u32,
    value: f64,
    timestamp: u64,
}

fn main() {
    let r1 = SensorReading { sensor_id: 1, value: 23.5, timestamp: 1000 };
    let r2 = r1.clone(); // Clone: create an independent copy

    println!("{:?}", r1);   // Debug: print struct contents
    println!("{:#?}", r2);  // Debug: pretty-printed format

    if r1 == r2 {           // PartialEq: compare two values
        println!("Readings are identical");
    }
}

Common derivable traits: Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord.

Generics: Writing Type-Agnostic Functions

Generics let you write functions and structs that work with any type. The compiler generates specialized code for each type used -- zero runtime cost.

// Find the maximum value in a slice of any comparable type
fn find_max<T: PartialOrd>(values: &[T]) -> Option<&T> {
    if values.is_empty() {
        return None;
    }
    let mut max = &values[0];
    for item in &values[1..] {
        if item > max {
            max = item;
        }
    }
    Some(max)
}

fn main() {
    let temperatures = vec![72.1, 85.4, 63.9, 91.0];
    let pressures = vec![3, 7, 2, 9, 4];

    println!("Max temp: {:?}", find_max(&temperatures));
    println!("Max pressure: {:?}", find_max(&pressures));
}

Generic structs work the same way:

struct Threshold<T> {
    min: T,
    max: T,
}

impl<T: PartialOrd + std::fmt::Display> Threshold<T> {
    fn check(&self, value: &T) -> &str {
        if value < &self.min { "BELOW" }
        else if value > &self.max { "ABOVE" }
        else { "OK" }
    }
}

Trait Bounds and where Clauses

Trait bounds constrain what types a generic can accept. For complex bounds, where clauses improve readability.

use std::fmt::Display;

// Inline trait bound
fn log_reading<T: Display>(label: &str, value: T) {
    println!("[LOG] {label}: {value}");
}

// Multiple bounds with +
fn log_and_compare<T: Display + PartialOrd>(a: T, b: T) {
    if a > b {
        println!("{a} exceeds {b}");
    }
}

// where clause -- cleaner for multiple generics
fn process_pair<A, B>(sensor: A, threshold: B)
where
    A: Display + Clone,
    B: Display + PartialOrd,
{
    println!("Sensor: {sensor}, Threshold: {threshold}");
}

impl Trait in Function Signatures

impl Trait is a shorthand for simple cases. Use it in parameters to accept any type implementing a trait, or in return types to hide the concrete type.

use std::fmt::Display;

// Accept any type that implements Display
fn print_sensor_value(val: &impl Display) {
    println!("Reading: {val}");
}

// Return some type that implements Iterator -- hides internal details
fn alarm_ids() -> impl Iterator<Item = u32> {
    (1..=10).filter(|id| id % 3 == 0)
}

fn main() {
    print_sensor_value(&42.5);
    print_sensor_value(&"offline");

    for id in alarm_ids() {
        println!("Active alarm: #{id}");
    }
}

impl Trait in return position is especially useful for closures and iterators where the actual type is complex or unnameable.

Practical Example: A Universal Sensor Logger

Combining traits and generics to build a reusable logging system:

use std::fmt;

// Trait for anything that provides a reading
trait Sensor: fmt::Display {
    fn read(&self) -> f64;
    fn sensor_id(&self) -> &str;
}

// Generic logger that works with any Sensor
struct SensorLogger<S: Sensor> {
    sensor: S,
    threshold: f64,
    log: Vec<f64>,
}

impl<S: Sensor> SensorLogger<S> {
    fn new(sensor: S, threshold: f64) -> Self {
        SensorLogger { sensor, threshold, log: Vec::new() }
    }

    fn record(&mut self) {
        let value = self.sensor.read();
        self.log.push(value);
        if value > self.threshold {
            println!("ALERT [{}]: {value} exceeds {}", self.sensor.sensor_id(), self.threshold);
        }
    }

    fn average(&self) -> Option<f64> {
        if self.log.is_empty() { return None; }
        Some(self.log.iter().sum::<f64>() / self.log.len() as f64)
    }
}

// Concrete sensor types
struct TempProbe { id: String, value: f64 }
struct FlowMeter { id: String, liters_per_min: f64 }

impl fmt::Display for TempProbe {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "TempProbe({}: {}°C)", self.id, self.value)
    }
}
impl Sensor for TempProbe {
    fn read(&self) -> f64 { self.value }
    fn sensor_id(&self) -> &str { &self.id }
}

impl fmt::Display for FlowMeter {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "FlowMeter({}: {} L/min)", self.id, self.liters_per_min)
    }
}
impl Sensor for FlowMeter {
    fn read(&self) -> f64 { self.liters_per_min }
    fn sensor_id(&self) -> &str { &self.id }
}

fn main() {
    let probe = TempProbe { id: "T-01".into(), value: 88.5 };
    let mut logger = SensorLogger::new(probe, 85.0);
    logger.record();

    let meter = FlowMeter { id: "F-03".into(), liters_per_min: 12.4 };
    let mut flow_log = SensorLogger::new(meter, 15.0);
    flow_log.record();
}

Summary

  • Traits define shared behavior as a contract that types implement.
  • Any type can implement any trait, giving Rust flexible polymorphism without inheritance.
  • #[derive] auto-generates common traits like Debug, Clone, and PartialEq.
  • Generics let you write functions and structs that work with many types at zero cost.
  • Trait bounds (inline or where clauses) constrain generics to types with required behavior.
  • impl Trait simplifies function signatures for both parameters and return types.
  • Combining traits and generics is the Rust pattern for building reusable industrial components.
traits generics trait-bounds impl-trait derive polymorphism السمات الأنواع العامة حدود السمات تعدد الأشكال التجريد إعادة الاستخدام