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, andPartialEq. - Generics let you write functions and structs that work with many types at zero cost.
- Trait bounds (inline or
whereclauses) 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.