Rust struct types — regular, tuple, and unit

4 min 659 words
ilhan Ben Martin Placeholder text describing the default author's avatar.


(Eng) Rust struct types — regular, tuple, and unit

Rust gives you three ways to define a struct. Each exists because sometimes naming a field is helpful, sometimes the position is enough, and sometimes you just need a named thing with no data at all.

struct Regular {
    red: u8,
    green: u8,
    blue: u8,
}

struct Tuple(u8, u8, u8);

struct Unit;

Regular struct — named fields

The standard form. Every field has a name and a type.

struct ColorRegularStruct {
    red: u8,
    green: u8,
    blue: u8,
}

let green = ColorRegularStruct {
    red: 0,
    green: 255,
    blue: 0,
};
println!("{}", green.red);  // 0

Named fields make the code self-documenting. When you see green.red, you know exactly what it is.

Tuple struct — positional fields

Fields have positions instead of names. Access them with .0, .1, .2 — same syntax as tuples.

struct ColorTupleStruct(u8, u8, u8);

let green = ColorTupleStruct(0, 255, 0);
println!("{}", green.0);  // 0

The difference from a bare tuple (u8, u8, u8) is that ColorTupleStruct is its own type. The compiler will not let you pass a ColorTupleStruct where a (u8, u8, u8) is expected, or confuse it with another tuple struct that happens to have the same field types.

struct Rgb(u8, u8, u8);
struct Hsv(f32, f32, f32);

let c = Rgb(255, 0, 0);
let d = Hsv(0.0, 1.0, 0.5);
// c = d;  // ❌ type mismatch — Rgb vs Hsv

The C embedded analogy

In C, you use struct for named fields and raw arrays for positional data:

// Named fields — like regular struct
struct sensor {
    uint32_t raw;
    uint8_t channel;
};

// Positional — like tuple struct
uint8_t rgb[3] = {0, 255, 0};
rgb[0];  // 0 — positional, but unchecked

The difference: struct ColorTupleStruct(u8, u8, u8) is a named type even though its fields are positional. A C array uint8_t[3] has no type identity — you can pass it anywhere that accepts any uint8_t*. A tuple struct stops you from mixing up RGB values with, say, accelerometer readings that happen to also be three u8s.

C conceptRust equivalentType safety
struct { uint8_t r, g, b; }Regular struct with named fields✅ Field access by name
uint8_t arr[3] with positional conventionTuple struct with positional fields✅ Named type, won't mix with other 3-byte tuples
typedef struct {} Empty zero-size markerUnit struct✅ Named type, zero bytes

Unit struct — the zero-size marker

struct UnitStruct;

A struct with no fields. It occupies zero bytes at runtime. The compiler knows it exists, but it carries no data.

let unit = UnitStruct;
println!("{:?}s are fun!", unit);  // UnitStructs are fun!

Embedded use cases. Unit structs are used as type-level markers:

  • Type-state patternstruct Connected; and struct Disconnected; as types on a handle. The compiler can enforce that you can't write to a disconnected port.
  • Trait implementations — when you need a type to implement a trait but have no data to store, a unit struct is the zero-overhead way.
trait Register {
    fn address(&self) -> u32;
}

struct AdcConfig;
impl Register for AdcConfig {
    fn address(&self) -> u32 { 0x4001_2000 }
}

struct DmaConfig;
impl Register for DmaConfig {
    fn address(&self) -> u32 { 0x4002_0000 }
}

Which one to use

SituationStruct type
Each field has a distinct meaningRegular struct with named fields
Position alone conveys the meaning (RGB, XY coordinates)Tuple struct
You need a distinct type with no data (type-state, trait marker)Unit struct
Quick one-off grouping within a functionBare tuple (u8, u8, u8)

The core lesson

The three struct forms are not redundancy. They give you different levels of type identity:

  • A bare tuple (u8, u8, u8) is anonymous — you can pass it anywhere that shape is accepted.
  • A tuple struct Color(u8, u8, u8) is a named type — the compiler enforces intent.
  • A regular struct Color { r, g, b } is named and self-documenting — the compiler enforces intent and the code tells you what each field means.

Pick the least amount of ceremony that still makes wrong code look wrong to the compiler.