---
title: Structs
source: 'https://academia.sh/en/courses/rust-fundamentals/structs'
course: 'Rust Fundamentals'
language: en
updated: '2026-09-15T04:58:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that all three syntaxes for constructing a struct value are expressions'
    - 'Explains that a struct declaration is an item and produces no value on its own'
    - 'Distinguishes a method from an associated function by the presence of a receiver'
    - 'Compares the identity of field order in a named struct versus a tuple struct'
---

# Structs

The same Point value is built three separate ways — full syntax, update syntax, associated function — and all three produce a value as an expression; the struct declaration itself produces no value, it only introduces a name the following lines can use.

The Control Flow lesson measured, one by one, whether each of five control-flow forms
produces a value: `if` and `match` were expressions, `loop` carried a value only through
`break`, and `while` and `for` never gave one. This lesson carries the same question to
different material. There are no longer control-flow forms but data types, and the
question does not change: is constructing a value from a type an expression, or is writing
the type itself to the source an expression?

In Rust, the way to gather several fields under one type is a struct. This lesson measures
the difference between a struct's **declaration** and the **construction** of a value from
that struct using three separate syntaxes — full syntax, update syntax, and an associated
function — and shows that all three fall into the same class. The declaration itself never
falls into that class. The second half of the lesson returns to what the declaration
contains, namely fields: the same set of fields behaves quite differently across two forms
of struct — the named and the unnamed — and that difference is measured separately.

## A Declaration Is an Item, Constructing a Value Is an Expression

The line `struct Point { x: i32, y: i32 }` produces nothing when it runs. This line is not
a computation but a declaration that introduces a name to the compiler: it is the
precondition for later lines being able to use the name `Point`, but it is not itself a
value. In the previous lesson's term, this is an **item** — it does not "run" at any point
in the source, it merely sits there and adds a type the compiler will know about.

By contrast, the syntax `Point { x: 3, y: 4 }` produces a value when it runs: it takes two
integers and packs them into a single value of type `Point`. This is an **expression** —
its result can be bound to a variable, passed as an argument to a function, appended to a
list. The difference here is exactly the distinction the previous lesson set up: one sits
there without being evaluated, the other is evaluated and gives back a value.

**DM1.** — The claim that a `struct` declaration by itself produces no output is not
verified here by a separate run but by the structure of the program below: the line
`struct Point { ... }` sits before `main`, not inside it, and no print call triggers that
line. The only output the program shows when it runs comes from the lines where the name
`Point` is **used**.

The reason behind this distinction is that the two kinds of lines are processed by the
compiler at different times. A declaration is read at compile time: the compiler records
the name `Point`, its fields, and their types into a table, but this recording leaves no
trace at the program's runtime. A value-construction syntax, however, acts at runtime:
when the line `Point { x: 3, y: 4 }` executes, two integers are actually assembled in
memory. This is the same order as a binding built with `let` (the subject of the Variables
and Immutability lesson) taking place at runtime — the difference is that what sits to the
left of a binding must be an expression, not an item.

This is one of the places where the three classes separate most clearly. A **statement**
is still evaluated — only its result falls to `()` or is discarded — but an **item** is
never evaluated at all; there is nothing that "runs" where it sits in the source. A
`struct` declaration falls into this third category: it is not a step passed over while the
program runs, but a record the compiler processes once while reading the source.

The run below places three different ways of constructing a value from the same `Point`
type side by side.

```rust
// struct_construction.rs — a struct declaration is an item, constructing a struct value is an expression
struct Point {
    x: i32,
    y: i32,
}

impl Point {
    fn origin() -> Point {
        Point { x: 0, y: 0 }
    }

    fn magnitude(&self) -> i32 {
        self.x.abs() + self.y.abs()
    }
}

fn main() {
    let a = Point { x: 3, y: 4 };
    println!("{:<21}: x={} y={}", "full syntax", a.x, a.y);

    let b = Point { y: 9, ..a };
    println!("{:<21}: x={} y={}", "update syntax", b.x, b.y);

    let c = Point::origin();
    println!("{:<21}: x={} y={}", "associated function", c.x, c.y);

    println!("{:<21}: {}", "method result", b.magnitude());
}
```

```
full syntax          : x=3 y=4
update syntax        : x=3 y=9
associated function  : x=0 y=0
method result        : 12
```

**Full syntax** gives every field one by one: `x` and `y` are written explicitly, and the
result is a value with `x=3 y=4`. **Update syntax** (`..a`) is a shorthand for that: only
the `y` field is given again, and the remaining fields are copied from `a`. The result is
still a single `Point` value produced in one call — the syntax differs, the class does not.
**The associated function** (`Point::origin()`) reaches the same class by an entirely
different route: without ever writing `x` and `y`, the function call itself produces a
`Point`. What the three syntaxes share is that all three can be bound to a name with
`let` — something an expression can do by definition, and something meaningless for an
item.

## Method vs. Associated Function: the Receiver's Presence

The calls `Point::origin()` and `b.magnitude()` look similar on the surface: both are
defined inside an `impl Point` block, and both return a value. The difference between them
shows up in the call syntax and comes down to a single point: **the presence of a
receiver**.

The `origin` function's signature has no `self`; it is therefore called through the type
name (`Point::origin()`) and requires no instance. In Rust's own terms this is an
**associated function** — it is associated with the `Point` type, but not with any
particular `Point` value. The `magnitude` function's signature, on the other hand, has
`&self` as its first parameter; this parameter states which value the call is being made
on, and it does not appear at the call site — the `b` in `b.magnitude()` takes its place.
This is what Rust calls a **method**.

The distinction is measured not by what the work does but by what the signature requires:
with a `self` parameter, the call goes through a value (method); without one, through the
type name (associated function). In the run above, `origin()` produced a value from
nothing, needing no existing `Point`; `magnitude()` could only be called through the
already-existing value `b`. Both are expressions — both return a value — and this adds no
new branch to the expression/statement/item axis: every Rust function call that does not
return `()` is already an expression, whether method or associated function. The
distinction lies outside that axis, in the call syntax, in whether a receiver is present.

The `magnitude` function computes the point's distance from the origin as the sum of two
absolute values: `self.x.abs() + self.y.abs()`. In the run, `b` carried the value `x=3
y=9`, so the `magnitude()` call gave `3 + 9 = 12`. `origin`, as its name suggests, plays the
role of a constructor: it is not the only one of the three ways to produce a `Point` value,
but a common naming habit — presenting a type's most common starting value through an
associated function like `Type::origin()`, `Type::empty()`, or `Type::default()`. There is
no limit at the declaration stage on how many such functions there can be; the `impl Point`
block gathers all of them in the same place.

Named-struct syntax asks for a field name only when constructing a value; a field name
never appears at a method call. In the syntax `b.magnitude()`, `b` states which value the
call is made on, but inside the `magnitude` function, the fields `self.x` and `self.y` are
reached by name. Even though the receiver appears reduced to a single dot (`.`) in the call
syntax, inside the function body it is the full `Point` value whose fields are reached by
name.

The `self` parameter carries a single `&` sign in front of it. The exact meaning of this
sign, and why the receiver can be used without being copied, is the subject of the
Ownership and the Memory Model course; what is measured here is only how the receiver's
**presence** determines the call syntax, not how the receiver is passed.

There is no upper bound on how many associated functions or methods a type's `impl` block
can hold. Even a small type like `Point` can have several constructor associated
functions — alongside `origin()`, names such as `unit() -> Point` or `pair(x: i32, y: i32)
-> Point` could offer other default values. Which names get used is a naming habit, not a
language rule; the compiler treats all of them as the same class, the associated function.

## Field Order Carries No Identity in a Named Struct

When constructing a struct value, the order in which the fields are written in the source
does not have to match the order in the declaration. `Point { x: 3, y: 4 }` and `Point { y:
4, x: 3 }` produce the same value; the compiler matches each field by its name, not its
position.

```rust
// field_order.rs — field order carries no identity in a named struct
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let d = Point { x: 3, y: 4 };
    let e = Point { y: 4, x: 3 };
    println!("{:<18}: x={} y={}", "d (x first)", d.x, d.y);
    println!("{:<18}: x={} y={}", "e (y first)", e.x, e.y);
    println!("{:<18}: {}", "d and e equal", d.x == e.x && d.y == e.y);
}
```

```
d (x first)       : x=3 y=4
e (y first)       : x=3 y=4
d and e equal     : true
```

`d` and `e` were written in different order in the source, but both carry the same `x` and
`y` values. A field name here is not a position marker but a matching key: the writing
order is a matter of readability preference, it does not change the meaning.

This is an exception that falls outside Rust's general call convention. In a function call,
arguments are matched by position — in the syntax `double(3)`, which parameter the `3`
goes to is decided by its position in the parameter list, not by a name. Struct-construction
syntax is the one exception to this: each `field: value` pair inside `{ }` finds its match
in the `struct` declaration by name. This privilege belongs only to **construction**
syntax; when accessing a struct value's field (as in `a.x`), it is already accessed by
name, and no "order" question ever arises there.

**DM2.** — The claim that "order does not change meaning" is measured here not by a
separate field-comparison method but directly by the line `d.x == e.x && d.y == e.y`; this
line reads that both values' fields are equal to each other one by one, it does not compare
`d` and `e` themselves.

## Position Is the Only Identity in a Tuple Struct

The exact opposite of the named struct is the tuple struct: its fields have no name, only a
position and a type. The declaration `Color(u8, u8, u8)` builds a type that carries three
bytes, but it is not name but **position** that decides which of these three bytes is "red"
and which is "blue."

```rust
// tuple_identity.rs — position is the only identity in a tuple struct
struct Color(u8, u8, u8);

fn main() {
    let red = Color(220, 20, 20);
    let Color(r1, g1, b1) = red;
    println!("{:<18}: r={} g={} b={}", "Color(220,20,20)", r1, g1, b1);

    let blue = Color(20, 20, 220);
    let Color(r2, g2, b2) = blue;
    println!("{:<18}: r={} g={} b={}", "Color(20,20,220)", r2, g2, b2);
}
```

```
Color(220,20,20)  : r=220 g=20 b=20
Color(20,20,220)  : r=20 g=20 b=220
```

This is the **limiting measurement**: while field order was free in a named struct (the
previous run showed this with `d` and `e`), position is the sole identity in a tuple
struct. `Color(220, 20, 20)` and `Color(20, 20, 220)` do not carry the same three numbers —
in the first, the first position is large; in the second, the third position is — and the
result is two different color values. In a named struct, a name answers the question "which
field"; in a tuple struct, only position answers that same question. In the writing
`Color(20, 20, 220)`, which field the first two `20`s belong to is resolved by looking at
**where** that value sits.

**DM3.** — The claim "position is the only identity" in a tuple struct is measured here by
placing the same three numbers into different positions, that is by producing the pair
`Color(220, 20, 20)` and `Color(20, 20, 220)`: the two calls carry the same three values (a
permutation of `20`, `20`, `220`), but because their positions differ, so do the results.

Destructuring also comes into play here: the line `let Color(r1, g1, b1) = red;` binds the
three fields of the value `red` in order to the names `r1`, `g1`, `b1`. This writing is in
fact a pattern — it matches the shape of the value on the right against the names on the
left — and the full scope of taking values apart with patterns is the subject of the next
lesson. Here it is only visible that accessing a tuple struct's fields goes through this
route: where `a.x` can be written for a named struct, a tuple struct's field is reached
either by position (`color.0`, `color.1`, `color.2`) or by destructuring, not by name.

A single-field tuple struct is also used to keep two quantities of the same underlying type
distinct from each other. `struct Pixel(i32);` and `struct Millimeter(i32);` both carry an
`i32` inside, but for the compiler they are two separate types; a `Pixel` value does not
stand in for a `Millimeter` value, even though its field looks the same. This is the tuple
struct's most common use: not giving an existing type a new name, but a new **identity**. A
named struct reaches the same goal with a field name (`Location { pixel: i32 }`); a tuple
struct does it with a single wrapper field, no field name ever written.

**DM4.** — The contrast between the named struct and the tuple struct is measured by
placing two separate runs side by side: `field_order.rs` changes the order and shows the
result stays the **same**, `tuple_identity.rs` changes the position and shows the result is
**different**. Both runs use the same three inputs (three numbers); what changes is only
whether the type is named or unnamed.

A struct is Rust's most direct way of holding data together: a fixed number of fields, each
tied to a single type, all present at once. The three syntaxes this lesson measured — full
syntax, update syntax, associated function — all arrive at the same result because all of
them fill the same fixed set of fields. But not every struct value is bound to a fixed set
of fields; the data a program has to model often takes more than one possible shape in an
"either this or that" way. A struct alone cannot meet this — a `Point` always carries both
`x` and `y`, there is no third "carries nothing" state. The next lesson builds the type
that fills exactly that gap.

## Summary

- A `struct` declaration is an item: it produces no value when it runs, it only introduces
  a name.
- All three syntaxes for constructing a struct value — full syntax, update syntax,
  associated function — are expressions and produce a value.
- The difference between a method and an associated function is the presence of a receiver
  (`self`): a method is called through a value, an associated function through a type name.
- Field order carries no meaning in a named struct; fields are matched by name.
- A tuple struct's fields have no name, only a position; position is the sole identity and
  the result changes when it is changed.

## Next Step

This lesson measured how many fields a struct value carries and how those fields are
constructed — but a struct value always carries the same set of fields. The next lesson
carries the same question to a different shape: can a type have more than one form, where
which fields it carries **changes depending on state**? An enum sets up a value being able
to carry one of several separate states under the same type; the next question is how these
states are read from the source.
