What a struct is¶
Level: 101 → 201 · for newcomers
One line: A struct names a group of values and makes that name a type. It holds no behaviour.
- Named fields, so nothing depends on position (unlike
(String, Vec<u8>)). - A new type. Kept apart from every other type, including an identical one under a different name.
- No behaviour. Methods go in an
implblock; shared behaviour in traits.
Data here, behaviour there¶
struct Player { … } // data
impl Player { // behaviour
fn new(name: &str) -> Self { … } // associated function — no self
fn total(&self) -> u32 { … } // method — takes self
}
Struct = record. impl = functions. trait = behaviour many types share. This is why there are no classes and no inheritance; traits do that job. Details: impl blocks.
Three flavors, and a fourth spelling¶
| Flavor | Written | Fields by |
|---|---|---|
| named-field | struct Player { name: String } |
.name |
| tuple struct | struct Level(u32); |
.0 |
| unit struct | struct Sealed; |
none |
A tuple struct is a named-field struct whose names are digits — Level { 0: 7 } compiles and equals Level(7).
Not "a classic C struct": layout is undefined by default (fields may be reordered), fields are private by default, assignment moves unless the type is Copy.
struct AlsoEmpty {} also has no fields but is not a unit struct — it needs braces:
error[E0423]: expected value, found struct `AlsoEmpty`
| println!("{:?}", AlsoEmpty);
| ^^^^^^^^^ help: use struct literal syntax instead: `AlsoEmpty {}`
A unit struct holds nothing, so behaviour is all it can hold. Use one when you need something to impl a trait on.
struct Sealed; declares two things sharing a name: the type Sealed and a constant value Sealed. A type alias aliases only the type:
type Alias = Sealed;
let a: Sealed = Alias {}; // fine — type namespace
let b: Sealed = Alias; // error[E0423]: expected value, found type alias `Alias`
Names¶
UpperCamelCase for types, snake_case for fields and methods — rustc lints both (non_camel_case_types, non_snake_case). Name the struct for what the group means: Player, not Data.
Privacy is per module¶
Private = visible inside the defining module, not "only to its own methods". Any code in that module reads any field.
For a tuple struct, a private field makes the constructor private:
error[E0603]: tuple struct constructor `Meters` is private
| a constructor is private if any of the fields is private
This is what the newtype relies on: one checked door.
What you cannot do¶
- Mark one field mutable.
mutis on the binding — all of it or none. - Put
&strin a field without a lifetime.error[E0106]: missing lifetime specifier. OwnedStringsidesteps it; that is why beginner code uses it. - Print with
{}. NoDisplayunless you write one. See Debug and Display.
If you are coming from another language¶
Python. Closest match is a @dataclass or NamedTuple. The field list looks the same, and self is explicit in both.
| Python | Rust | |
|---|---|---|
| wrong field name | AttributeError at runtime |
build error |
| add a field at runtime | obj.new_field = 1 works |
impossible; the field set is the type |
| data + methods | one class body |
struct + impl, separate |
__slots__ is the nearest Python analogue to a fixed field set, but it is opt-in and Rust's is not.
ABAP. TYPES: BEGIN OF ty_player, name TYPE string, ... END OF ty_player. maps almost directly; DATA ls_player TYPE ty_player is the instance.
| ABAP | Rust | |
|---|---|---|
| methods on the type | none — behaviour goes in a class beside it | impl block |
| flat vs deep | a distinction you manage | none; a String field always owns its data |
| uninitialised | initial value, free | must supply every field or it does not compile |
impl is the half ABAP has no place for, which is why a Rust struct behaves more like a small class than like a TYPES group.
Practice¶
Three flavors, and the two things the compiler keeps apart.
- Define
Color(i32, i32, i32)andPoint(i32, i32, i32), write a function taking one, pass it the other. ReadE0308— then note what a bare(i32, i32, i32)would have accepted. - Put a tuple struct in a module with
pubon the struct but not the field; construct it from outside.E0603: "a constructor is private if any of the fields is private." Why does that follow, rather than being an extra rule? - Give a unit struct a
Displayimpl — a type whose only content is behaviour.
Solution
what_a_struct_is_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: three flavors, and the two things the compiler keeps apart.
//!
//! rustc --edition 2024 what_a_struct_is_kata.rs -o /tmp/wasik && /tmp/wasik
use std::fmt;
// Same three fields, same types, two different TYPES. This is the whole
// point of a tuple struct over a bare tuple: the name is load-bearing.
#[derive(Debug)]
struct Color(i32, i32, i32);
#[derive(Debug)]
struct Point(i32, i32, i32);
fn paint(c: &Color) -> String {
format!("#{:02x}{:02x}{:02x}", c.0, c.1, c.2)
}
mod sealed {
// `pub` on the STRUCT is not `pub` on the FIELD, and for a tuple struct
// that also makes the CONSTRUCTOR private — you cannot write Meters(9)
// from outside this module, because doing so would set a private field.
#[derive(Debug)]
pub struct Meters(u32);
impl Meters {
pub fn new(n: u32) -> Self {
Meters(n)
}
pub fn get(&self) -> u32 {
self.0
}
}
}
// A unit struct carries no data, so the only thing it can be is a place to
// hang behaviour. That is not a consolation prize — it is the use case.
struct Blank;
impl fmt::Display for Blank {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "(nothing selected)")
}
}
fn main() {
println!("1. Identical shapes, different types");
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
println!(" Color(0,0,0) -> {}", paint(&black));
println!(" {origin:?} is the same THREE i32s and paint(&origin) will not compile:");
println!(" error[E0308]: mismatched types — expected `&Color`, found `&Point`");
println!(" A bare (i32, i32, i32) would have accepted either. The name is the guard.");
println!("\n2. Destructuring — the tuple struct comes apart like a tuple");
let Color(r, g, b) = black;
println!(" let Color(r, g, b) = black; -> r={r} g={g} b={b}");
let Point(x, y, z) = origin;
println!(" let Point(x, y, z) = origin; -> x={x} y={y} z={z}");
println!(" Same shape, same destructuring, and still not interchangeable.");
println!("\n3. A private field makes a tuple struct's CONSTRUCTOR private");
let distance = sealed::Meters::new(431);
println!(" sealed::Meters::new(431) -> {distance:?}, get() = {}", distance.get());
println!(" sealed::Meters(431) from out here is E0603:");
println!(" `a constructor is private if any of the fields is private`");
println!(" That is the newtype's whole guarantee: one door, and the module owns it.");
println!("\n4. A unit struct holds nothing, so behaviour is all it can hold");
println!(" {Blank} <- via its Display impl; the value carries no data at all");
}
Verified output of what_a_struct_is_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. Identical shapes, different types
Color(0,0,0) -> #000000
Point(0, 0, 0) is the same THREE i32s and paint(&origin) will not compile:
error[E0308]: mismatched types — expected `&Color`, found `&Point`
A bare (i32, i32, i32) would have accepted either. The name is the guard.
2. Destructuring — the tuple struct comes apart like a tuple
let Color(r, g, b) = black; -> r=0 g=0 b=0
let Point(x, y, z) = origin; -> x=0 y=0 z=0
Same shape, same destructuring, and still not interchangeable.
3. A private field makes a tuple struct's CONSTRUCTOR private
sealed::Meters::new(431) -> Meters(431), get() = 431
sealed::Meters(431) from out here is E0603:
`a constructor is private if any of the fields is private`
That is the newtype's whole guarantee: one door, and the module owns it.
4. A unit struct holds nothing, so behaviour is all it can hold
(nothing selected) <- via its Display impl; the value carries no data at all
The verified output¶
Verified output of what_a_struct_is.rs — regenerated by tools/run_examples.py, never hand-typed.
1. Three flavors, and a fourth spelling
named Player { name: "Ada", scores: [] }
tuple Level(7) field reached by index: 7
unit Sealed — the type has exactly one value
braces AlsoEmpty — `AlsoEmpty {}`, NOT the same as a unit struct
2. A tuple struct is a named-field struct whose fields are numbers
Level { 0: 7 } == Level(7) -> true
3. Field ORDER in the expression is free; the value is the same
Player { name: "Ben", scores: [5, 2] }
Player { name: "Ben", scores: [5, 2] }
same value: true
4. Data in the struct, behaviour in the impl block
Cara scored [5, 2, 0], total 7
Player::total(&player) == player.total() -> true
5. Privacy is per module — `count` is private, and that is why
the module has to hand you a method to read it
id (pub) 12
count (private) 431 <- via count(), not s.count
s.count would be E0616: field `count` of struct `Sealed` is private
See also¶
Po polsku¶
Struktura to nazwa dla grupy wartości, która sama staje się typem — i nic ponadto, bo zachowania nie trzyma żadnego. Dane siedzą w struct, funkcje w bloku impl, a zachowanie wspólne dla wielu typów w cesze (trait); dlatego w Ruscie nie ma klas ani dziedziczenia. Polskiego czytelnika najłatwiej zmyli tu samo słowo, bo „strukturę” większość z nas poznała w C, gdzie jest ona rekordem o ustalonym układzie w pamięci. Rustowa struktura to co innego: układ pól jest domyślnie nieokreślony (kompilator może je poprzestawiać — jeśli potrzebujesz gwarancji, żądasz jej przez #[repr(C)]), pola są domyślnie prywatne, a przypisanie przenosi własność, o ile typ nie jest Copy.
Odmiany są trzy plus jedna pisownia na doczepkę: struktura nazwana (Player { name: String }, pola po nazwie), struktura krotkowa (Level(u32), pola po numerze — Level { 0: 7 } kompiluje się i równa Level(7), bo krotkowa to po prostu nazwana struktura, której pola nazywają się cyframi) oraz pusta struktura (struct Sealed;), która nie mieści nic, więc jedyne, co może nieść, to zachowanie — i po to właśnie się ją pisze. Uwaga na czwartą pisownię: struct AlsoEmpty {} też nie ma pól, ale pustą strukturą nie jest i przy użyciu domaga się klamer, inaczej dostaniesz E0423 z podpowiedzią „use struct literal syntax instead: AlsoEmpty {}”. Przy okazji widać wtedy rzecz, o której podręczniki zwykle nie wspominają: struct Sealed; deklaruje dwie rzeczy o jednej nazwie — typ i stałą wartość — bo Rust prowadzi osobne przestrzenie nazw dla typów i dla wartości. Alias type Alias = Sealed; kopiuje tylko tę pierwszą, więc Alias {} przejdzie, a samo Alias już nie.
Nazwa typu jest w Ruscie strażnikiem, bo typowanie jest nominalne, a nie strukturalne: Color(i32, i32, i32) i Point(i32, i32, i32) mają identyczny kształt i mimo to nie da się jednego podać tam, gdzie oczekiwany jest drugi — E0308, podczas gdy goła krotka (i32, i32, i32) przyjęłaby oba bez słowa. Prywatność, jak zwykle w Ruscie, obowiązuje na poziomie modułu, a nie typu, i w strukturze krotkowej ma dodatkowy skutek: prywatne pole czyni prywatnym sam konstruktor (E0603, „a constructor is private if any of the fields is private”) — na tym stoi cały wzorzec newtype. Na koniec drobiazg dla piszących po polsku: typy nazywamy w UpperCamelCase, pola w snake_case (pilnują tego linty non_camel_case_types i non_snake_case), a polskie znaki diakrytyczne w identyfikatorach są dozwolone — struct Gracz { imię: String } kompiluje się bez mrugnięcia (sprawdzone na rustc 1.98, edycja 2024). Praktyka jest jednak inna: kod trzyma się angielskiego, bo po angielsku są komunikaty, dokumentacja i wyniki wyszukiwania.
Szukaj po polsku: struktura krotkowa · pusta struktura · prywatność na poziomie modułu · rust tuple struct vs tuple E0308 · rust repr(C) struct layout