Structs and Enums in Rust: Modeling Factory Data
Structs: Custom Data Types
In industrial software, we constantly model real-world objects: sensors, machines, alarms. Rust structs let us group related data into a single, meaningful type.
// A temperature sensor on a production line
struct TemperatureSensor {
id: u32,
location: String,
current_reading: f64, // in Celsius
is_active: bool,
}
fn main() {
let sensor = TemperatureSensor {
id: 101,
location: String::from("Furnace Zone A"),
current_reading: 482.5,
is_active: true,
};
println!("Sensor {} at {}: {}°C", sensor.id, sensor.location, sensor.current_reading);
}
Each field has a name and a type. You access fields with dot notation like sensor.id.
impl Blocks: Adding Behavior
Structs hold data, but impl blocks attach functions (methods) to them.
struct PressureGauge {
max_pressure: f64, // bar
current_pressure: f64,
}
impl PressureGauge {
// Associated function (constructor) - no &self
fn new(max_pressure: f64) -> Self {
PressureGauge {
max_pressure,
current_pressure: 0.0,
}
}
// Method - takes &self to read data
fn is_over_limit(&self) -> bool {
self.current_pressure > self.max_pressure
}
// Method - takes &mut self to modify data
fn update_reading(&mut self, value: f64) {
self.current_pressure = value;
}
}
fn main() {
let mut gauge = PressureGauge::new(10.0);
gauge.update_reading(12.3);
if gauge.is_over_limit() {
println!("WARNING: Pressure exceeds maximum!");
}
}
Tuple Structs and Unit Structs
Tuple structs have fields without names. They are useful for creating distinct types from simple values. Unit structs have no fields at all.
// Tuple structs - distinct types wrapping values
struct Millimeters(f64);
struct Celsius(f64);
// Unit struct - used as a marker or signal
struct EmergencyStop;
fn calibrate_thickness(reading: Millimeters) {
println!("Thickness: {} mm", reading.0); // access by index
}
fn main() {
let thickness = Millimeters(2.45);
let temp = Celsius(95.0);
calibrate_thickness(thickness);
// calibrate_thickness(temp); // Compile error! Different type.
}
Enums: Types with Variants
An enum defines a type that can be one of several variants. This is perfect for states, modes, or categories in industrial systems.
enum MachineState {
Idle,
Running,
Maintenance,
Faulted,
}
fn describe_state(state: &MachineState) {
match state {
MachineState::Idle => println!("Machine is idle"),
MachineState::Running => println!("Machine is running"),
MachineState::Maintenance => println!("Under maintenance"),
MachineState::Faulted => println!("FAULT detected"),
}
}
Enums with Data: Rust's Power Feature
Unlike many languages, Rust enum variants can carry data. Each variant can hold different types and amounts of data.
enum SensorReading {
Temperature(f64), // single value
Vibration { x: f64, y: f64 }, // named fields
Offline, // no data
Error(u32, String), // multiple values
}
fn log_reading(reading: &SensorReading) {
match reading {
SensorReading::Temperature(t) => println!("Temp: {t}°C"),
SensorReading::Vibration { x, y } => println!("Vibration: x={x}, y={y}"),
SensorReading::Offline => println!("Sensor offline"),
SensorReading::Error(code, msg) => println!("Error {code}: {msg}"),
}
}
match with Enums: Exhaustive Handling
Rust's match forces you to handle every variant. The compiler will reject
code that misses a case, preventing bugs before they reach production.
enum ConveyorSpeed {
Stop,
Slow,
Normal,
Fast,
}
fn get_rpm(speed: &ConveyorSpeed) -> u32 {
match speed {
ConveyorSpeed::Stop => 0,
ConveyorSpeed::Slow => 200,
ConveyorSpeed::Normal => 600,
ConveyorSpeed::Fast => 1200,
}
// Removing any arm causes a compile error!
}
You can also use _ as a wildcard to catch all remaining variants.
Practical Example: Factory Alarm System
Combining structs and enums to build a factory alarm model:
enum AlarmSeverity {
Info,
Warning,
Critical,
}
enum AlarmSource {
Sensor(u32),
Machine(String),
ProductionLine(u8),
}
struct Alarm {
severity: AlarmSeverity,
source: AlarmSource,
message: String,
}
impl Alarm {
fn should_stop_line(&self) -> bool {
matches!(self.severity, AlarmSeverity::Critical)
}
fn display(&self) {
let level = match &self.severity {
AlarmSeverity::Info => "INFO",
AlarmSeverity::Warning => "WARN",
AlarmSeverity::Critical => "CRIT",
};
let origin = match &self.source {
AlarmSource::Sensor(id) => format!("Sensor #{id}"),
AlarmSource::Machine(name) => format!("Machine: {name}"),
AlarmSource::ProductionLine(n) => format!("Line {n}"),
};
println!("[{level}] {origin} - {}", self.message);
}
}
fn main() {
let alarm = Alarm {
severity: AlarmSeverity::Critical,
source: AlarmSource::Machine(String::from("CNC-07")),
message: String::from("Spindle overheated"),
};
alarm.display();
if alarm.should_stop_line() {
println!(">>> STOPPING PRODUCTION LINE <<<");
}
}
Summary
- Structs group related fields into custom types that model real-world objects.
- impl blocks attach methods and constructors to structs.
- Tuple structs create distinct types from simple values; unit structs carry no data.
- Enums define types with multiple variants, each optionally carrying data.
- match ensures every variant is handled, eliminating forgotten cases at compile time.
- Combining structs and enums is the foundation for modeling industrial systems in Rust.