Structs

Named-Field Structs

A named-field struct (often called a C-style struct or standard struct) is the most common struct variety in Rust. It defines a composite type whose fields each have an explicit identifier and type enclosed in curly braces { field_name: Type, ... }.

Syntax and Field Naming

Every field in a named-field struct is written in snake_case followed by a colon (:) and its type, separated by commas (with a trailing comma after the last field when written across multiple lines):

#[derive(Debug)]
struct ServerEndpoint {
    hostname: String,
    port: u16,
    tls_enabled: bool,
    max_connections: usize,
}

fn main() {
    let primary = ServerEndpoint {
        hostname: String::from("api.example.com"),
        port: 443,
        tls_enabled: true,
        max_connections: 1024,
    };

    println!(
        "Connecting to {}:{} (TLS: {}, max: {})",
        primary.hostname, primary.port, primary.tls_enabled, primary.max_connections
    );
}
Connecting to api.example.com:443 (TLS: true, max: 1024)

Notice that a named-field struct declaration ends with its closing curly brace }—it never takes a trailing semicolon after }.

Why Named Fields Prevent Positional Argument Bugs

When a record has two or more fields of the same primitive type—such as width: u32 and height: u32, or latitude: f64 and longitude: f64—a tuple (u32, u32) makes it easy to accidentally swap the order of arguments ((height, width)).

A named-field struct eliminates positional mistakes because every field is labeled by name at both construction and access sites, and fields can be initialized in any order:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    // Field order in the initializer does not have to match declaration order:
    let screen = Rectangle {
        height: 1080,
        width: 1920,
    };

    println!("Resolution: {}x{}", screen.width, screen.height);
}
Resolution: 1920x1080

Memory Representation and Field Reordering (#[repr(Rust)])

In C (struct), the compiler is forced to lay out struct fields in memory in the exact order they are written in source code, which can waste memory on alignment padding when 1-byte and 8-byte fields alternate.

By default, Rust uses the repr(Rust) representation, which allows rustc to automatically reorder struct fields in memory to minimize alignment padding:

Source Declaration:
  struct Mixed {
      a: u8,    (1 byte, align 1)
      b: u64,   (8 bytes, align 8)
      c: u8,    (1 byte, align 1)
  }

In C (Fixed Field Order -> 24 bytes total!):
  +----+-----------------------+----------------------------------+----+-----------------------+
  | a  | 7 bytes wasted pad    |            b (8 bytes)           | c  | 7 bytes wasted pad    |
  +----+-----------------------+----------------------------------+----+-----------------------+

In Rust (Default `repr(Rust)` Reorders `b` First -> Only 16 bytes total!):
  +----------------------------------+----+----+------------------+
  |            b (8 bytes)           | a  | c  | 6 bytes tail pad |
  +----------------------------------+----+----+------------------+
use std::mem::size_of;

struct MixedRust {
    a: u8,
    b: u64,
    c: u8,
}

#[repr(C)]
struct MixedC {
    a: u8,
    b: u64,
    c: u8,
}

fn main() {
    println!("size_of::<MixedRust>() (optimized layout) = {} bytes", size_of::<MixedRust>());
    println!("size_of::<MixedC>()    (#[repr(C)] order) = {} bytes", size_of::<MixedC>());
}
size_of::<MixedRust>() (optimized layout) = 16 bytes
size_of::<MixedC>()    (#[repr(C)] order) = 24 bytes

When passing a struct across a Foreign Function Interface (FFI) boundary to C code, annotate the struct with #[repr(C)] so rustc preserves C's exact field ordering and padding rules.

Destructuring and Pattern Matching on Named-Field Structs

Named-field structs can be unpacked directly in let, if let, match, and function parameters using { field1, field2 } patterns (or { field1, .. } to ignore remaining fields):

struct Account {
    id: u64,
    username: String,
    balance_cents: i64,
}

fn describe_account(acc: &Account) {
    match acc {
        Account { username, balance_cents: 0, .. } => {
            println!("{username} has a zero balance");
        }
        Account { username, balance_cents, .. } if *balance_cents < 0 => {
            println!("{username} is overdrawn by {} cents", -balance_cents);
        }
        Account { id, username, balance_cents } => {
            println!("Account #{id} ({username}): {balance_cents} cents");
        }
    }
}

fn main() {
    let user = Account {
        id: 7,
        username: String::from("ferris"),
        balance_cents: 2500,
    };
    describe_account(&user);
}
Account #7 (ferris): 2500 cents