9.5 Tuple Structs and Unit-Like Structs
Besides named-field structs, Rust has two other variants.
9.5.1 Tuple Structs
Tuple structs have a name but unnamed fields, defined using parentheses ()
. Access fields using index notation (.0
, .1
, etc.).
struct Color(u8, u8, u8); // Represents RGB struct Point2D(f64, f64); // Represents coordinates fn main() { let black = Color(0, 0, 0); let origin = Point2D(0.0, 0.0); println!("Red component: {}", black.0); println!("Y-coordinate: {}", origin.1); }
Tuple structs are useful when the field names are obvious from the context or when you want to give a tuple a distinct type name, improving type safety. Even if two tuple structs have the same field types, they are considered different types.
9.5.2 The Newtype Pattern
A common and powerful use case for tuple structs with a single field is the newtype pattern. This involves wrapping an existing type (like i32
, f64
, or even String
) in a new struct to create a distinct type. This pattern provides two main benefits:
- Enhanced Type Safety: It prevents accidental mixing of values that have the same underlying representation but different semantic meanings.
- Implementing Traits: It allows you to implement traits (which define behaviors) specifically for your new type, even if the underlying type already has implementations or you’re not allowed to implement the trait for the base type directly (due to Rust’s orphan rule).
Example: Type Safety with Units
Consider representing distances. Using plain integers could lead to errors if units are mixed.
// Add derive for Debug, Copy, Clone, PartialEq for easier use in examples #[derive(Debug, Copy, Clone, PartialEq)] struct Millimeters(u32); #[derive(Debug, Copy, Clone, PartialEq)] struct Meters(u32); fn main() { let length_mm = Millimeters(5000); let length_m = Meters(5); // The compiler prevents mixing these types, even though both wrap a u32: // print_length_mm(length_m); // Compile Error! Expected Millimeters, found Meters print_length_mm(length_mm); // OK } fn print_length_mm(mm: Millimeters) { // We access the inner value using tuple index syntax `.0` println!("Length: {} mm", mm.0); }
Even though both Millimeters
and Meters
internally hold a u32
, the compiler treats them as distinct types, enforcing unit correctness at compile time.
Example: Implementing Behavior (Traits)
A key advantage is adding specific behaviors. Let’s allow Millimeters
values to be added together or multiplied by a scalar factor by implementing the standard Add
and Mul
traits.
use std::ops::{Add, Mul}; // Import the traits #[derive(Debug, Copy, Clone, PartialEq)] // Added Copy for Add example struct Millimeters(u32); // Implement the `Add` trait for Millimeters impl Add for Millimeters { type Output = Self; // Adding two Millimeters results in Millimeters // self: Millimeters, other: Millimeters fn add(self, other: Self) -> Self::Output { // Add the inner u32 values and wrap the result in a new Millimeters Millimeters(self.0 + other.0) } } // Implement the `Mul` trait for multiplying Millimeters by a u32 scalar impl Mul<u32> for Millimeters { type Output = Self; // Multiplying Millimeters by u32 results in Millimeters // self: Millimeters, factor: u32 fn mul(self, factor: u32) -> Self::Output { // Multiply the inner u32 value and wrap the result Millimeters(self.0 * factor) } } fn main() { let len1 = Millimeters(150); let len2 = Millimeters(75); // Use the implemented Add trait let total_length = len1 + len2; println!("{:?} + {:?} = {:?}", len1, len2, total_length); // Output: Millimeters(150) + Millimeters(75) = Millimeters(225) // Use the implemented Mul trait let factor = 3; let scaled_length = len1 * factor; println!("{:?} * {} = {:?}", len1, factor, scaled_length); // Output: Millimeters(150) * 3 = Millimeters(450) // Note: We did not implement adding Millimeters to Meters, // nor multiplying Millimeters by Millimeters. The type system // still prevents operations we haven't explicitly defined. // let m = Meters(1); // let invalid = len1 + m; // Compile Error! Cannot add Meters to Millimeters }
The newtype pattern, therefore, allows you to leverage Rust’s strong type system not just for passive checks but also to define precisely which operations are valid and meaningful for your custom types, enhancing both safety and code clarity. This is particularly useful for modeling domain-specific units, identifiers, or other constrained values.
9.5.3 Unit-Like Structs
Unit-like structs have no fields. They are defined simply with struct StructName;
.
#[derive(Debug, PartialEq, Eq)] // Added derive for comparison struct Marker; // A unit-like struct, often used as a marker fn main() { let m1 = Marker; let m2 = Marker; // These instances occupy no memory (zero-sized type) println!("Markers are equal: {}", m1 == m2); // true }
They are useful as markers or when implementing a trait that doesn’t require associated data.