Structs
Data · Lesson 20
Structs group named fields into one type of your own.
Coming from GoA Rust struct is a Go struct with name: Type order. You must set every field when you build one (there are no zero values), and fields are private to their module until you mark them pub, not when they start with a lowercase letter. There is no struct embedding: a struct that holds another one keeps it in a named field.
Coming from KotlinA struct is a data class where you don't write the constructor: you build it with User { name: ..., age: ... }. Printing, == and copying are opt-in through #[derive(Debug, PartialEq, Clone)] rather than given for free.
Coming from PythonA struct is your @dataclass, and #[derive(Debug)] is the generated __repr__. The set of fields is fixed, so you can't add a new attribute to a value later.
Coming from TypeScriptA struct is like an interface plus an object literal, but every field must be given and the type is nominal. Another struct with the same fields is still not a User, and an optional field is written Option<T> (Option), not age?:.
Coming from JavaA Rust struct is closest to a record: named fields, and every field must be given when you create one. Unlike a record, a field can be changed if the variable is mut. There's no constructor, no default values and no auto toString or equals; #[derive(Debug, PartialEq)] adds them.
Coming from C#A struct with #[derive(Debug)] is a lot like a positional record, but you build it with field names, like an object initializer where every property is required. There is no constructor and no default value for a missing field. It is not a C# struct in the value-type sense: let b = a; moves the value, it does not copy it.
Coming from CRust structs look like C structs built with designated initializers, .name = ... becoming name: .... Leaving a field out is a compile error instead of zero, and #[derive(Debug)] writes the print function for you. Rust may also reorder the fields in memory; #[repr(C)] asks for C's layout.
Coming from C++A Rust struct is an aggregate built with designated initializers, but every field is required, so nothing is silently zeroed. #[derive(Debug)] stands in for the operator<< you would write by hand. Rust may reorder the fields in memory; #[repr(C)] asks for the order you wrote.
The quick comparison
#[derive(Debug)] // lets you print it with {:?}struct User { name: String, age: u32, active: bool,}
fn main() { let u = User { name: String::from("ana"), age: 30, active: true, }; println!("{} is {}, active={}", u.name, u.age, u.active); println!("{u:?}");}ana is 30, active=true
User { name: "ana", age: 30, active: true }package main
import "fmt"
type User struct { Name string Age int Active bool}
func main() { u := User{Name: "ana", Age: 30, Active: true} fmt.Printf("%s is %d, active=%t\n", u.Name, u.Age, u.Active) fmt.Printf("%+v\n", u)}ana is 30, active=true
{Name:ana Age:30 Active:true}data class User( val name: String, val age: Int, val active: Boolean,)
fun main() { val u = User(name = "ana", age = 30, active = true) println("${u.name} is ${u.age}, active=${u.active}") println(u)}ana is 30, active=true User(name=ana, age=30, active=true)
from dataclasses import dataclass
@dataclass # also gives a readable repr, like #[derive(Debug)]class User: name: str age: int active: bool
u = User(name="ana", age=30, active=True)print(f"{u.name} is {u.age}, active={u.active}")print(u)ana is 30, active=True User(name='ana', age=30, active=True)
interface User { name: string; age: number; active: boolean;}
const u: User = { name: "ana", age: 30, active: true,};console.log(`${u.name} is ${u.age}, active=${u.active}`);console.log(u);ana is 30, active=true
{ name: 'ana', age: 30, active: true }public class Main { // a record gets a constructor, getters and toString for free record User(String name, int age, boolean active) {}
public static void main(String[] args) { var u = new User("ana", 30, true); System.out.println(u.name() + " is " + u.age() + ", active=" + u.active()); System.out.println(u); }}ana is 30, active=true User[name=ana, age=30, active=true]
var u = new User("ana", 30, true);Console.WriteLine($"{u.Name} is {u.Age}, active={u.Active}");Console.WriteLine(u); // records print their fields for free
record User(string Name, uint Age, bool Active);ana is 30, active=True
User { Name = ana, Age = 30, Active = True }#include <stdbool.h>#include <stdio.h>
struct User { const char *name; unsigned age; bool active;};
int main(void) { struct User u = { .name = "ana", .age = 30, .active = true, }; printf("%s is %u, active=%s\n", u.name, u.age, u.active ? "true" : "false"); // C has no {:?}: print each field yourself printf("User { name: \"%s\", age: %u, active: %s }\n", u.name, u.age, u.active ? "true" : "false"); return 0;}ana is 30, active=true
User { name: "ana", age: 30, active: true }#include <iostream>#include <string>
struct User { std::string name; unsigned age; bool active;};
// C++ has no derive: you write the printing yourselfstd::ostream& operator<<(std::ostream& os, const User& u) { return os << "User { name: \"" << u.name << "\", age: " << u.age << ", active: " << std::boolalpha << u.active << " }";}
int main() { User u{.name = "ana", .age = 30, .active = true}; std::cout << u.name << " is " << u.age << ", active=" << std::boolalpha << u.active << "\n"; std::cout << u << "\n";}ana is 30, active=true
User { name: "ana", age: 30, active: true }What's different
#[derive(Debug)]asks the compiler to write the printing code for you, so{u:?}can print the whole struct. You can also deriveClone,PartialEq(for==),Default, and more.String::from("ana")makes an ownedStringfrom text in your code. The Strings lesson, later, explains the two string types.User { age: 31, ..old }builds a new value and takes every field you did not list fromold. If it takes aStringor other owned field,oldloses it.- Fields are private outside their module unless marked
pub. There is no capital-letter rule.
Leaving fields out
Looks fine: in Go, Java, C#, C and C++ it runs fine; in Python it crashes at runtime. Rust won't compile it.
Works in Go. In Rust it's just a rule.Kotlin refuses this too. Same idea, different words.Broken in Python. Rust catches it.TypeScript refuses this too. Same idea, different words.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.struct User { name: String, age: u32, active: bool,}
fn main() { let u = User { name: String::from("ana"), }; println!("{}", u.name);}package main
import "fmt"
type User struct { Name string Age int Active bool}
func main() { u := User{Name: "ana"} fmt.Println(u.Name, u.Age, u.Active)}ana 0 false
Kotlin refuses this too: leaving out age and active is a compile error unless the class gives them defaults, like val age: Int = 0. The closest Rust tool is #[derive(Default)], which defaults the whole struct, shown in the fix.
data class User(val name: String, val age: Int, val active: Boolean)
fun main() { val u = User(name = "ana") println(u.name)}Main.kt:4:13: error: no value passed for parameter 'age'.
val u = User(name = "ana")
^^^^
Main.kt:4:13: error: no value passed for parameter 'active'.
val u = User(name = "ana")
^^^^from dataclasses import dataclass
@dataclassclass User: name: str age: int active: bool
u = User(name="ana")print(u.name)Traceback (most recent call last):
File "main.py", line 11, in <module>
u = User(name="ana")
TypeError: User.__init__() missing 2 required positional arguments: 'age' and 'active'TypeScript refuses this too: an object typed as User must have every field, so age and active are reported as missing.
interface User { name: string; age: number; active: boolean;}
const u: User = { name: "ana" };console.log(u.name);main.ts(7,7): error TS2739: Type '{ name: string; }' is missing the following properties from type 'User': age, activepublic class Main { static class User { String name; int age; boolean active; }
public static void main(String[] args) { var u = new User(); u.name = "ana"; System.out.println(u.name + " " + u.age + " " + u.active); }}ana 0 false
var u = new User { Name = "ana" };Console.WriteLine($"{u.Name} {u.Age} {u.Active}");
class User{ public string Name { get; set; } = ""; public uint Age { get; set; } public bool Active { get; set; }}ana 0 False
#include <stdbool.h>#include <stdio.h>
struct User { const char *name; unsigned age; bool active;};
int main(void) { struct User u = {.name = "ana"}; printf("%s %u %d\n", u.name, u.age, u.active); return 0;}ana 0 0
#include <iostream>#include <string>
struct User { std::string name; unsigned age; bool active;};
int main() { User u{.name = "ana"}; std::cout << u.name << " " << u.age << " " << std::boolalpha << u.active << "\n";}main.cpp: In function ‘int main()’:
main.cpp:11:25: warning: missing initializer for member ‘User::age’ [-Wmissing-field-initializers]
11 | User u{.name = "ana"};
| ^
main.cpp:11:25: warning: missing initializer for member ‘User::active’ [-Wmissing-field-initializers]ana 0 false
What does the compiler say? Guess first, then open.
error[E0063]: missing fields `active` and `age` in initializer of `User` --> src/main.rs:8:13 | 8 | let u = User { | ^^^^ missing `active` and `age`
Go runs this and prints ana 0 false. Missing fields get their zero values, which Go treats as a feature, and many Go types are written so that the zero value works. Rust asks you to say it.
Kotlin refuses this too: leaving out age and active is a compile error unless the class gives them defaults, like val age: Int = 0. The closest Rust tool is #[derive(Default)], which defaults the whole struct, shown in the fix.
A dataclass won't let you skip fields either. Python raises a TypeError when you build the object, naming the two missing fields.
TypeScript refuses this too: an object typed as User must have every field, so age and active are reported as missing.
A plain class fills missing fields with 0 and false, so this prints ana 0 false like Go. A record would refuse, because its constructor needs every field.
The object initializer fills only Name, and the rest get defaults, so it prints ana 0 False. Mark the properties required if you want the compiler to insist.
C sets the fields you leave out to zero, so this prints ana 0 0. Rust makes you give every field.
This compiles with a -Wextra warning about the missing fields, then prints ana 0 false. With brace initialization the left-out fields are zero, much like Go.
Rust has no automatic zero values. Every field must be given.
Nothing here is unsafe; it is a different default. Is age: 0 a real age, or a field you forgot? Is port: 0 intended? Rust makes you say. When zeros really are what you want, ask for them explicitly with Default. (Go would use a *int for "unset"; Rust uses Option<u32>, Option.)
The fix
Compiles and runs.
#[derive(Default)]struct User { name: String, age: u32, active: bool,}
fn main() { // struct update syntax (explained in the lesson): the rest comes from Default let u = User { name: String::from("ana"), ..Default::default() }; println!("{} {} {}", u.name, u.age, u.active);}ana 0 false
The toolkit
A struct groups several values under one name, each with a name of its own. It is how you make a type for a real thing in your program: a user, a point, a config. Outputs on this page come from Linux on 64-bit ARM with Rust 1.99.1; where a fact depends on the machine or the compiler version, the text says so.
Defining and building a struct
You list the fields and their types once. Then you build values by giving every field a value:
struct Book { title: String, pages: u32, read: bool,}
fn main() { // Fields can be given in any order. let mut book = Book { pages: 310, title: String::from("Dune"), read: false, }; println!("title: {}", book.title); println!("pages: {}", book.pages); book.read = true; println!("read: {}", book.read);}title: Dune pages: 310 read: true
- Fields can be written in any order. Reading is
book.title. - Every field is required. Nothing is filled in for you, not even zero. The gotcha above shows the error. When you want defaults, you ask for them on purpose (see derive).
- You cannot add a field later. A name that is not in the definition is a compile error:
struct User { name: String,}
fn main() { let mut u = User { name: String::from("ana"), }; u.nickname = String::from("an"); println!("{}", u.name);}error[E0609]: no field `nickname` on type `User` --> src/main.rs:9:7 | 9 | u.nickname = String::from("an"); | ^^^^^^^^ unknown field | = note: available field is: `name`
A tuple is the quick way to return two or three values. Reach for a struct when the parts need names, or when you want a type of your own that the compiler will not mix up with another.
mut belongs to the whole variable. You cannot write mut on one field:
struct Book { title: String, read: bool,}
fn main() { let book = Book { title: String::from("Dune"), read: false, }; book.read = true; println!("{} read: {}", book.title, book.read);}error[E0594]: cannot assign to `book.read`, as `book` is not declared as mutable --> src/main.rs:11:5 | 11 | book.read = true; | ^^^^^^^^^^^^^^^^ cannot assign | help: consider changing this to be mutable | 7 | let mut book = Book { | +++
(A few special types, such as Cell, can be changed through a variable that is not mut, on purpose. Smart pointers covers them.)
Field init shorthand
When a variable has the same name as the field, write the name once. This is common in functions that build a struct from their arguments:
#[derive(Debug)]struct Book { title: String, pages: u32, read: bool,}
fn new_book(title: String, pages: u32) -> Book { // `title` alone means `title: title`. Book { title, pages, read: false, }}
fn main() { let book = new_book(String::from("Dune"), 310); println!("book: {book:?}"); println!( "{} has {} pages, read: {}", book.title, book.pages, book.read );}book: Book { title: "Dune", pages: 310, read: false }
Dune has 310 pages, read: falseRust has no constructors. A plain function that returns the struct does the job. By habit it is called new, and it lives in an impl block, which the next lesson covers.
Building one struct from another
Struct update syntax takes the fields you did not list from another value of the same struct. ..base goes last:
#[derive(Clone)]struct Config { name: String, retries: u32, verbose: bool,}
fn show(label: &str, c: &Config) { println!( "{label}: name={}, retries={}, verbose={}", c.name, c.retries, c.verbose );}
fn main() { let base = Config { name: String::from("base"), retries: 3, verbose: false, };
// `name` is given, so `..base` only supplies `retries` and `verbose`. // Both are copied, so `base` stays whole. let loud = Config { name: String::from("loud"), verbose: true, ..base }; show("loud", &loud); show("base", &base);
// `..base.clone()` supplies `name` as a copy, so `base` is never touched. let renamed = Config { verbose: true, ..base.clone() }; show("renamed", &renamed); show("base", &base);}loud: name=loud, retries=3, verbose=true base: name=base, retries=3, verbose=false renamed: name=base, retries=3, verbose=true base: name=base, retries=3, verbose=false
In the first case, base is still whole because ..base only took retries and verbose, which are copied. If ..base has to supply a String or any other owned value, that value is moved out of base, as in let b = a; (Ownership). Then base can’t be used as a whole:
#[derive(Debug)]struct Config { name: String, retries: u32, verbose: bool,}
fn main() { let base = Config { name: String::from("base"), retries: 3, verbose: false, };
// `name` is not given, so `..base` moves it out of `base`. let loud = Config { verbose: true, ..base }; println!("loud: {loud:?}"); println!("base: {base:?}");}error[E0382]: borrow of partially moved value: `base` --> src/main.rs:21:22 | 16 | let loud = Config { | ________________- 17 | | verbose: true, 18 | | ..base 19 | | }; | |_____- value partially moved here 20 | println!("loud: {loud:?}"); 21 | println!("base: {base:?}"); | ^^^^ value borrowed here after partial move | = note: partial move occurs because `base.name` has type `String`, which does not implement the `Copy` trait
To keep base, take a copy with ..base.clone(), as the second case does. ..Default::default() fills the rest from the struct’s defaults, as in the gotcha’s fix.
Tuple structs and unit structs
A tuple struct has fields without names, reached by position. A unit struct has no fields at all:
struct Meters(f64);struct Rgb(u8, u8, u8);struct Marker;
fn main() { let length = Meters(3.5); let red = Rgb(255, 0, 0); let _marker = Marker;
println!("length: {}", length.0); println!("red: {}, {}, {}", red.0, red.1, red.2);
let Rgb(r, g, b) = red; println!("r={r} g={g} b={b}");
println!("size of Meters: {} bytes", size_of::<Meters>()); println!("size of f64: {} bytes", size_of::<f64>()); println!("size of Marker: {} bytes", size_of::<Marker>());}length: 3.5 red: 255, 0, 0 r=255 g=0 b=0 size of Meters: 8 bytes size of f64: 8 bytes size of Marker: 0 bytes
A tuple struct with one field is called a newtype. Here Meters takes no more space than the f64 inside it (8 bytes, on this compiler), but it is a new type to the compiler, so a function that wants Meters will not accept feet by accident:
struct Meters(f64);struct Feet(f64);
fn describe(distance: Meters) -> String { format!("{} meters", distance.0)}
fn main() { let altitude = Feet(1200.0); println!("{}", describe(altitude));}error[E0308]: mismatched types --> src/main.rs:10:29 | 10 | println!("{}", describe(altitude)); | -------- ^^^^^^^^ expected `Meters`, found `Feet` | | | arguments to this function are incorrect | note: function defined here --> src/main.rs:4:4 | 4 | fn describe(distance: Meters) -> String { | ^^^^^^^^ ----------------
A unit struct takes no space. It is used as a marker, or to hang behaviour on once you meet traits.
Nested structs
A field can be another struct. Chain the dots to reach inside, and to change it:
struct Point { x: i32, y: i32,}
struct Circle { center: Point, radius: u32,}
fn main() { let mut c = Circle { center: Point { x: 1, y: -2 }, radius: 5, }; println!("x: {}", c.center.x); c.center.x += 10; c.radius = 8; println!( "center: ({}, {}), radius: {}", c.center.x, c.center.y, c.radius ); println!("size of Point: {} bytes", size_of::<Point>()); println!("size of Circle: {} bytes", size_of::<Circle>());}x: 1 center: (11, -2), radius: 8 size of Point: 8 bytes size of Circle: 12 bytes
The inner struct lives inside the outer one, in the same block of memory. The sizes show it: a Point is two i32s (8 bytes), and a Circle is that plus a u32 (12), with no room for a pointer.
Derive: asking the compiler for standard behaviour
A new struct can’t do much: it can’t be printed, compared or copied. #[derive(…)] above a struct asks the compiler to write that code for you, using the fields. Each name in the list is a trait: a named ability a type can have. Traits come later; for now each one is a switch.
| Derive | What you get | Needs |
|---|---|---|
Debug |
{:?} and {:#?} printing |
every field is Debug |
Clone |
.clone(), a full copy |
every field is Clone |
Copy |
assignment copies instead of moving (needs Clone too) |
every field is Copy, so no String or Vec |
PartialEq |
== and !=, field by field |
every field is PartialEq |
Eq |
signals that every value is equal to itself | every field is Eq, so no f64 |
PartialOrd, Ord |
<, > and sorting, field by field in the order you declared them |
every field has it; Ord also excludes f64 |
Hash |
use in a HashSet, or as a HashMap key (with Eq) |
every field is Hash |
Default |
Type::default(), with 0, false and "" |
every field has a default |
#[derive(Debug, Clone, PartialEq, Default)]struct Player { name: String, score: u32,}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]struct Version { major: u32, minor: u32,}
#[derive(PartialEq)]struct Reading { celsius: f64,}
fn main() { let a = Player { name: String::from("ana"), score: 10, }; let b = a.clone(); println!("a == b: {}", a == b); let c = Player { score: 11, ..a.clone() }; println!("a == c: {}", a == c); println!("a != c: {}", a != c); println!("default: {:?}", Player::default());
let v1 = Version { major: 1, minor: 9 }; let v2 = Version { major: 2, minor: 0 }; let v3 = Version { major: 1, minor: 10, }; println!("1.9 < 2.0: {}", v1 < v2); println!("1.10 > 1.9: {}", v3 > v1); let copy = v1; // Copy: `v1` is not moved println!("v1 == copy: {}", v1 == copy);
let r = Reading { celsius: f64::NAN }; println!("NaN reading == itself: {}", r == r);}a == b: true
a == c: false
a != c: true
default: Player { name: "", score: 0 }
1.9 < 2.0: true
1.10 > 1.9: true
v1 == copy: true
NaN reading == itself: false1.10 > 1.9 is true because the derived comparison looks at major first, then minor, as numbers. The Partial versions exist because f64 has a value, NaN (“not a number”), that is not equal to itself, as the last line shows. A struct holding an f64 can derive PartialEq but not Eq: that is a compile error (E0277). And #[derive(Copy)] on a struct with a String is refused (E0204), because a bitwise copy of a String would mean two owners of one heap block.
Hash plus Eq lets a struct be an item in a set, or a key in a map (covered later):
use std::collections::HashSet;
#[derive(Debug, PartialEq, Eq, Hash)]struct Point { x: i32, y: i32,}
fn main() { let mut seen = HashSet::new(); seen.insert(Point { x: 1, y: 2 }); seen.insert(Point { x: 1, y: 2 }); seen.insert(Point { x: 3, y: 4 }); println!("distinct points: {}", seen.len()); println!("has (3, 4): {}", seen.contains(&Point { x: 3, y: 4 }));}distinct points: 2 has (3, 4): true
Printing: {:?} and {:#?}
{} needs a trait called Display, which you write yourself (Common traits, later). {:?} uses Debug, which the compiler can write for you. {:#?} is the same on many lines, which helps with nested structs:
#[derive(Debug)]struct Point { x: i32, y: i32,}
#[derive(Debug)]struct Circle { center: Point, radius: u32, label: String,}
fn main() { let c = Circle { center: Point { x: 1, y: -2 }, radius: 5, label: String::from("home"), }; println!("radius {}, label {}", c.radius, c.label); println!("center ({}, {})", c.center.x, c.center.y); println!("{{:?}} -> {c:?}"); println!("{{:#?}} -> {c:#?}");}radius 5, label home
center (1, -2)
{:?} -> Circle { center: Point { x: 1, y: -2 }, radius: 5, label: "home" }
{:#?} -> Circle {
center: Point {
x: 1,
y: -2,
},
radius: 5,
label: "home",
}Without the derive, the error says what to add:
struct Point { x: i32, y: i32,}
fn main() { let p = Point { x: 1, y: 2 }; println!("{} {}", p.x, p.y); println!("{p:?}");}error[E0277]: `Point` doesn't implement `Debug` --> src/main.rs:9:15 | 9 | println!("{p:?}"); | ^^^^^ `Point` cannot be formatted using `{:?}` because it doesn't implement `Debug` | = help: the trait `Debug` is not implemented for `Point` help: consider annotating `Point` with `#[derive(Debug)]` | 1 + #[derive(Debug)] 2 | struct Point {
Debug output is for programmers: logs, assert_eq! failures, quick checks. The standard library docs say derived Debug formats are not stable and may change in future Rust versions, so don’t parse them.
Passing structs to functions
A struct follows the borrowing rules you already know, as a whole. The signature tells the reader what the function may do: &Account reads it, &mut Account changes it, Account takes it over.
struct Account { owner: String, balance: i64,}
fn summary(a: &Account) -> String { format!("{} has {}", a.owner, a.balance)}
fn deposit(a: &mut Account, amount: i64) { a.balance += amount;}
fn owner_name(a: &Account) -> String { a.owner.clone()}
fn main() { let mut acct = Account { owner: String::from("ana"), balance: 100, }; println!("{}", summary(&acct)); deposit(&mut acct, 50); println!("{}", summary(&acct)); let name = owner_name(&acct); println!("copied name: {name}"); println!("owner is still in acct: {}", acct.owner);}ana has 100 ana has 150 copied name: ana owner is still in acct: ana
Field access looks through a reference, so a.owner works whether a is an Account or a &Account. Passing the struct by value moves it, and the caller has nothing left:
struct Order { customer: String, items: Vec<String>,}
fn ship(order: Order) { println!("shipping {} items to {}", order.items.len(), order.customer);}
fn main() { let order = Order { customer: String::from("ana"), items: vec![String::from("tea")], }; ship(order); println!("customer: {}", order.customer);}error[E0382]: borrow of moved value: `order` --> src/main.rs:16:30 | 11 | let order = Order { | ----- move occurs because `order` has type `Order`, which does not implement the `Copy` trait ... 15 | ship(order); | ----- value moved here 16 | println!("customer: {}", order.customer); | ^^^^^^^^^^^^^^ value borrowed here after move | note: consider changing this parameter type in function `ship` to borrow instead if owning the value isn't necessary --> src/main.rs:6:16 | 6 | fn ship(order: Order) { | ---- ^^^^^ this parameter takes ownership of the value | | | in this function note: if `Order` implemented `Clone`, you could clone the value --> src/main.rs:1:1 | 1 | struct Order { | ^^^^^^^^^^^^ consider implementing `Clone` for this type ... 15 | ship(order); | ----- you could clone this value
The fix is the one from Borrowing: take &Order, unless the function really needs to keep it.
Structs that own data, and structs that borrow
Every struct so far owns its fields. A String field owns its text, a Vec<String> field owns its list, and when the struct goes away its fields are freed with it. This is the default, and the right choice for most structs.
A field can also be a reference, like &str, which borrows text owned by someone else. Rust must then know that the struct will not outlive that text. You write that down with a lifetime, which is the later lesson Lifetimes. Until then, the compiler asks for it:
struct Excerpt { text: &str,}
fn main() { let e = Excerpt { text: "call me Ishmael", }; println!("{}", e.text);}error[E0106]: missing lifetime specifier --> src/main.rs:2:11 | 2 | text: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct Excerpt<'a> { 2 ~ text: &'a str,
The 'a in the suggestion is a lifetime name. Don’t worry about it yet. Use String and Vec fields until you meet lifetimes.
Who can see the fields
By default, a field can be used only by code in the same module, or in modules nested inside it. A module is a named group of code (mod shop { … }; a file can be a module too). Mark a field pub to let code anywhere use it. Privacy is per field, and a struct can mix both kinds:
mod shop { pub struct Item { pub name: String, price_cents: u32, }
pub fn make_item(name: &str, price_cents: u32) -> Item { Item { name: name.to_string(), price_cents, } }}
fn main() { let item = shop::make_item("tea", 450); println!("name: {}", item.name); println!("price: {}", item.price_cents);}error[E0616]: field `price_cents` of struct `Item` is private --> src/main.rs:18:32 | 18 | println!("price: {}", item.price_cents); | ^^^^^^^^^^^ private field
Code inside shop can read price_cents:
mod shop { pub struct Item { pub name: String, price_cents: u32, }
pub fn make_item(name: &str, price_cents: u32) -> Item { Item { name: name.to_string(), price_cents, } }
// Code in the same module may read and change private fields. pub fn price(item: &Item) -> u32 { item.price_cents }
pub fn discount(item: &mut Item, cents: u32) { item.price_cents -= cents; }}
fn main() { let mut item = shop::make_item("tea", 450); println!("name: {}", item.name); println!("price: {}", shop::price(&item)); shop::discount(&mut item, 100); println!("price after discount: {}", shop::price(&item));}name: tea price: 450 price after discount: 350
Code outside the module can’t build an Item with a literal either, because a literal must name every field, private ones included (E0451). The module offers a function like make_item instead. So a private field can be changed later without breaking code outside the module.
Structs in your language
Going deeper
Moving a struct, and moving out of one
A struct you own can be taken apart one field at a time. Moving a field out is a partial move. The other fields stay usable, but the struct as a whole no longer is:
struct Order { customer: String, items: Vec<String>, id: u32,}
fn main() { let order = Order { customer: String::from("ana"), items: vec![String::from("tea")], id: 7, }; let before = order.customer.as_ptr();
let customer = order.customer; // moves one field out println!("customer: {customer}"); println!("same heap bytes: {}", customer.as_ptr() == before);
// The other fields are still there. println!("id: {}", order.id); println!("items: {}", order.items.len());}customer: ana same heap bytes: true id: 7 items: 1
order sit together in one struct; they are listed on separate rows here to show what moved. Only order.customer moved. The text on the heap did not (same heap bytes: true), and order.id and order.items are still usable. The three numbers of a String are drawn as ptr, len, cap; the order inside the real String is not specified.Using the struct whole is the part that no longer works:
struct Order { customer: String, items: Vec<String>,}
fn count(order: &Order) -> usize { order.items.len()}
fn main() { let order = Order { customer: String::from("ana"), items: vec![String::from("tea")], }; let customer = order.customer; println!("customer: {customer}"); println!("items: {}", count(&order));}error[E0382]: borrow of partially moved value: `order` --> src/main.rs:17:33 | 15 | let customer = order.customer; | -------------- value partially moved here 16 | println!("customer: {customer}"); 17 | println!("items: {}", count(&order)); | ^^^^^^ value borrowed here after partial move | = note: partial move occurs because `order.customer` has type `String`, which does not implement the `Copy` trait
Behind a reference you can’t move fields out at all, because the owner would be left with a hole. This is the error you get from fn owner_name(a: &Account) -> String { a.owner }:
struct Account { owner: String, balance: i64,}
fn owner_name(a: &Account) -> String { a.owner}
fn main() { let acct = Account { owner: String::from("ana"), balance: 100, }; println!("{} ({})", owner_name(&acct), acct.balance);}error[E0507]: cannot move out of `a.owner` which is behind a shared reference --> src/main.rs:7:5 | 7 | a.owner | ^^^^^^^ move occurs because `a.owner` has type `String`, which does not implement the `Copy` trait | help: consider cloning the value if the performance cost is acceptable | 7 | a.owner.clone() | ++++++++
The compiler’s suggestion, .clone(), is the simplest fix, and borrow-struct.rs above uses it. If you only need to look at the text, borrow it (&a.owner).
What a struct looks like in memory
A struct is its fields placed next to each other in one block. For a struct with a String, the block holds the String’s three numbers, and the text lives on the heap:
name takes the first 24 bytes, age the next 4 and active 1 more, then 3 bytes of padding (the offsets printed below). The three numbers inside String are drawn as ptr, len, cap; their real order is not specified. When u goes out of scope, the heap bytes are freed with it.The program prints the numbers behind the picture:
use std::mem::{align_of, offset_of, size_of};
struct User { name: String, age: u32, active: bool,}
fn main() { let u = User { name: String::from("ana"), age: 30, active: true, };
println!( "User: size {}, align {}", size_of::<User>(), align_of::<User>() ); println!("String: size {}", size_of::<String>()); println!( "offsets: name {}, age {}, active {}", offset_of!(User, name), offset_of!(User, age), offset_of!(User, active) ); println!( "name: len {}, capacity {}, bytes {:?}", u.name.len(), u.name.capacity(), u.name.as_bytes() ); println!("age: {}, active: {}", u.age, u.active);}User: size 32, align 8 String: size 24 offsets: name 0, age 24, active 28 name: len 3, capacity 3, bytes [97, 110, 97] age: 30, active: true
offset_of! is the byte position of a field inside its struct. The fields add up to 24 + 4 + 1 = 29 bytes, but User is 32. The missing 3 bytes are padding.
Size, alignment and padding
Every type has an alignment: a value of alignment n can only be stored at an address that is a multiple of n. A struct is laid out so that each field sits at an address that suits it, and the whole struct’s size is a multiple of its alignment. The gaps this leaves are padding.
In what order do the fields go? Take u8, u32, u8, in that order:
use std::mem::{align_of, offset_of, size_of};
// The fields are never read, only measured.#[allow(dead_code)]struct Plain { a: u8, b: u32, c: u8,}
#[allow(dead_code)]#[repr(C)]struct CLayout { a: u8, b: u32, c: u8,}
fn main() { println!("u8: size {}, align {}", size_of::<u8>(), align_of::<u8>()); println!( "u32: size {}, align {}", size_of::<u32>(), align_of::<u32>() ); println!( "u64: size {}, align {}", size_of::<u64>(), align_of::<u64>() ); println!("fields add up to {} bytes", 1 + 4 + 1);
println!( "Plain: size {}, align {}", size_of::<Plain>(), align_of::<Plain>() ); println!( " offsets: a {}, b {}, c {}", offset_of!(Plain, a), offset_of!(Plain, b), offset_of!(Plain, c) );
println!( "CLayout: size {}, align {}", size_of::<CLayout>(), align_of::<CLayout>() ); println!( " offsets: a {}, b {}, c {}", offset_of!(CLayout, a), offset_of!(CLayout, b), offset_of!(CLayout, c) );}u8: size 1, align 1 u32: size 4, align 4 u64: size 8, align 8 fields add up to 6 bytes Plain: size 8, align 4 offsets: a 4, b 0, c 5 CLayout: size 12, align 4 offsets: a 0, b 4, c 8
CLayout has the fields in the order written: a at 0, 3 bytes of padding so b can sit at 4, c at 8, and padding at the end to reach a multiple of 4. That is 12 bytes for 6 bytes of data. Plain is 8, because rustc put b first, then a and c, and the padding shrank.
#[repr(C)], which fixes the order to the one you wrote.What is promised and what is only what happened here? The Rust Reference says:
- Promised, default layout: fields do not overlap, each is aligned, and the size is a multiple of the alignment.
- Not promised, default layout: the order of the fields, so also the offsets and the 8 against 12. Those are rustc 1.99.1 on this machine. The compiler is free to reorder to save space, and here it did.
- Promised with
#[repr(C)]: the order you wrote, with the padding C would add.
Use #[repr(C)] when another program expects exactly this layout, such as C code you call. For files and network messages, write the fields one at a time: padding bytes hold no meaningful value, and byte order is yours to choose. Otherwise leave the default.
For one struct, the saving is nothing. For ten million in a Vec it is 4 bytes each here, which is 40 MB. You never need to choose it, and your code should not depend on the field order or the size unless you wrote #[repr(C)].
Practice
Try it
Each exercise is a small program with checks in main. Open it in the Playground, replace the todo!(), and run it until it prints all checks passed. The checks include the case people get wrong.
Update a struct, and ask for defaults
Write rename and quieter with struct update syntax, and fresh, which asks for the defaults. Note which of the first two needs .clone(), and why the other does not.
#[derive(Debug, Clone, PartialEq, Default)]struct Settings { name: String, volume: u8, muted: bool,}
/// Same settings under a new name. `s` is used up.fn rename(s: Settings, name: &str) -> Settings { todo!("give `name` as a String (name.to_string()), and take the other fields with `..s`")}
/// A copy of `s` with the volume halved (rounding down) and `muted` set to false./// `s` itself is left as it was.fn quieter(s: &Settings) -> Settings { todo!("set volume and muted, and take `name` from `..s.clone()`")}
/// Settings with this name and every other field at its default (0 and false).fn fresh(name: &str) -> Settings { todo!("give `name` as a String, and `..Default::default()` for the rest")}
fn main() { let lounge = Settings { name: String::from("lounge"), volume: 9, muted: true, }; let q = quieter(&lounge); assert_eq!( q, Settings { name: String::from("lounge"), volume: 4, muted: false } ); // The original is untouched. assert_eq!(lounge.volume, 9); assert!(lounge.muted); assert_eq!(lounge.name, "lounge");
let blank = fresh("attic"); assert_eq!(blank.name, "attic"); assert_eq!(blank.volume, 0); assert!(!blank.muted); assert_eq!(fresh("").name, "");
let kitchen = rename(lounge, "kitchen"); assert_eq!(kitchen.name, "kitchen"); assert_eq!(kitchen.volume, 9); assert!(kitchen.muted);
// Edge cases: volume 1 halves to 0, and names can be empty. let tiny = Settings { name: String::new(), volume: 1, muted: false, }; assert_eq!(quieter(&tiny).volume, 0); assert_eq!(quieter(&tiny).name, ""); assert_eq!(rename(tiny, "").name, "");
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
#[derive(Debug, Clone, PartialEq, Default)]struct Settings { name: String, volume: u8, muted: bool,}
/// Same settings under a new name. `s` is used up.fn rename(s: Settings, name: &str) -> Settings { Settings { name: name.to_string(), ..s }}
/// A copy of `s` with the volume halved (rounding down) and `muted` set to false./// `s` itself is left as it was.fn quieter(s: &Settings) -> Settings { Settings { volume: s.volume / 2, muted: false, ..s.clone() }}
/// Settings with this name and every other field at its default (0 and false).fn fresh(name: &str) -> Settings { Settings { name: name.to_string(), ..Default::default() }}
fn main() { let lounge = Settings { name: String::from("lounge"), volume: 9, muted: true, }; let q = quieter(&lounge); assert_eq!( q, Settings { name: String::from("lounge"), volume: 4, muted: false } ); // The original is untouched. assert_eq!(lounge.volume, 9); assert!(lounge.muted); assert_eq!(lounge.name, "lounge");
let blank = fresh("attic"); assert_eq!(blank.name, "attic"); assert_eq!(blank.volume, 0); assert!(!blank.muted); assert_eq!(fresh("").name, "");
let kitchen = rename(lounge, "kitchen"); assert_eq!(kitchen.name, "kitchen"); assert_eq!(kitchen.volume, 9); assert!(kitchen.muted);
// Edge cases: volume 1 halves to 0, and names can be empty. let tiny = Settings { name: String::new(), volume: 1, muted: false, }; assert_eq!(quieter(&tiny).volume, 0); assert_eq!(quieter(&tiny).name, ""); assert_eq!(rename(tiny, "").name, "");
println!("all checks passed");}all checks passed
Rectangles by reference
Write area, contains and shift for a Rect with a Point inside it. They must also work for the huge rectangle at the end of main, and contains has to treat the edges the right way round.
#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
#[derive(Debug, Clone, PartialEq)]struct Rect { top_left: Point, width: u32, height: u32,}
/// Width times height, for any rectangle, including the huge one at the end of `main`.fn area(r: &Rect) -> u64 { todo!("the result is a u64: convert width and height first")}
/// True when `p` is inside `r`. The left and top edges count as inside; the right and/// bottom edges do not. A rectangle with no width or no height contains nothing.fn contains(r: &Rect, p: Point) -> bool { todo!("x + width can overflow an i32: convert with i64::from(..) first (`as` can wrap)")}
/// Move the rectangle by `dx` and `dy`, which may be negative.fn shift(r: &mut Rect, dx: i32, dy: i32) { todo!("change the fields of r.top_left")}
fn main() { let mut r = Rect { top_left: Point { x: 2, y: 3 }, width: 4, height: 2, }; assert_eq!(area(&r), 8);
assert!(contains(&r, Point { x: 2, y: 3 })); // top-left corner assert!(contains(&r, Point { x: 5, y: 4 })); // last cell inside assert!(!contains(&r, Point { x: 6, y: 4 })); // right edge is outside assert!(!contains(&r, Point { x: 5, y: 5 })); // bottom edge is outside assert!(!contains(&r, Point { x: 1, y: 3 }));
shift(&mut r, -5, 1); assert_eq!(r.top_left, Point { x: -3, y: 4 }); assert!(contains(&r, Point { x: -3, y: 4 })); assert!(contains(&r, Point { x: 0, y: 5 })); assert!(!contains(&r, Point { x: 1, y: 5 }));
let flat = Rect { top_left: Point { x: 0, y: 0 }, width: 5, height: 0, }; assert_eq!(area(&flat), 0); assert!(!contains(&flat, Point { x: 0, y: 0 }));
let huge = Rect { top_left: Point { x: i32::MAX, y: i32::MIN, }, width: u32::MAX, height: u32::MAX, }; assert_eq!(area(&huge), 18_446_744_065_119_617_025); assert!(contains( &huge, Point { x: i32::MAX, y: i32::MIN } ));
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
#[derive(Debug, Clone, PartialEq)]struct Rect { top_left: Point, width: u32, height: u32,}
/// Width times height, for any rectangle, including the huge one at the end of `main`.fn area(r: &Rect) -> u64 { u64::from(r.width) * u64::from(r.height)}
/// True when `p` is inside `r`. The left and top edges count as inside; the right and/// bottom edges do not. A rectangle with no width or no height contains nothing.fn contains(r: &Rect, p: Point) -> bool { let left = i64::from(r.top_left.x); let top = i64::from(r.top_left.y); let right = left + i64::from(r.width); let bottom = top + i64::from(r.height); let (x, y) = (i64::from(p.x), i64::from(p.y)); left <= x && x < right && top <= y && y < bottom}
/// Move the rectangle by `dx` and `dy`, which may be negative.fn shift(r: &mut Rect, dx: i32, dy: i32) { r.top_left.x += dx; r.top_left.y += dy;}
fn main() { let mut r = Rect { top_left: Point { x: 2, y: 3 }, width: 4, height: 2, }; assert_eq!(area(&r), 8);
assert!(contains(&r, Point { x: 2, y: 3 })); // top-left corner assert!(contains(&r, Point { x: 5, y: 4 })); // last cell inside assert!(!contains(&r, Point { x: 6, y: 4 })); // right edge is outside assert!(!contains(&r, Point { x: 5, y: 5 })); // bottom edge is outside assert!(!contains(&r, Point { x: 1, y: 3 }));
shift(&mut r, -5, 1); assert_eq!(r.top_left, Point { x: -3, y: 4 }); assert!(contains(&r, Point { x: -3, y: 4 })); assert!(contains(&r, Point { x: 0, y: 5 })); assert!(!contains(&r, Point { x: 1, y: 5 }));
let flat = Rect { top_left: Point { x: 0, y: 0 }, width: 5, height: 0, }; assert_eq!(area(&flat), 0); assert!(!contains(&flat, Point { x: 0, y: 0 }));
let huge = Rect { top_left: Point { x: i32::MAX, y: i32::MIN, }, width: u32::MAX, height: u32::MAX, }; assert_eq!(area(&huge), 18_446_744_065_119_617_025); assert!(contains( &huge, Point { x: i32::MAX, y: i32::MIN } ));
println!("all checks passed");}all checks passed
A struct that owns its data
Write add, describe and checkout for a Basket that owns a String and a Vec<String>. checkout moves both fields out. The empty basket needs its own message.
struct Basket { owner: String, items: Vec<String>,}
/// Put `item` in the basket.fn add(b: &mut Basket, item: &str) { todo!("push a String made from item")}
/// "ana: empty", "ana: 1 item (tea)" or "ana: 2 items (tea, jam)".fn describe(b: &Basket) -> String { todo!("check b.items.is_empty() and b.items.len(), and use b.items.join(\", \")")}
/// Hand back the owner and the items. The basket is used up.fn checkout(b: Basket) -> (String, Vec<String>) { todo!("move both fields out")}
fn main() { let mut b = Basket { owner: String::from("ana"), items: Vec::new(), }; assert_eq!(describe(&b), "ana: empty");
add(&mut b, "tea"); assert_eq!(describe(&b), "ana: 1 item (tea)");
add(&mut b, "jam"); assert_eq!(describe(&b), "ana: 2 items (tea, jam)");
let (owner, items) = checkout(b); assert_eq!(owner, "ana"); assert_eq!(items, ["tea", "jam"]);
// An empty basket checks out with nothing. let (owner, items) = checkout(Basket { owner: String::new(), items: Vec::new(), }); assert_eq!(owner, ""); assert!(items.is_empty());
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
struct Basket { owner: String, items: Vec<String>,}
/// Put `item` in the basket.fn add(b: &mut Basket, item: &str) { b.items.push(item.to_string());}
/// "ana: empty", "ana: 1 item (tea)" or "ana: 2 items (tea, jam)".fn describe(b: &Basket) -> String { match b.items.len() { 0 => format!("{}: empty", b.owner), 1 => format!("{}: 1 item ({})", b.owner, b.items[0]), n => format!("{}: {n} items ({})", b.owner, b.items.join(", ")), }}
/// Hand back the owner and the items. The basket is used up.fn checkout(b: Basket) -> (String, Vec<String>) { (b.owner, b.items)}
fn main() { let mut b = Basket { owner: String::from("ana"), items: Vec::new(), }; assert_eq!(describe(&b), "ana: empty");
add(&mut b, "tea"); assert_eq!(describe(&b), "ana: 1 item (tea)");
add(&mut b, "jam"); assert_eq!(describe(&b), "ana: 2 items (tea, jam)");
let (owner, items) = checkout(b); assert_eq!(owner, "ana"); assert_eq!(items, ["tea", "jam"]);
// An empty basket checks out with nothing. let (owner, items) = checkout(Basket { owner: String::new(), items: Vec::new(), }); assert_eq!(owner, ""); assert!(items.is_empty());
println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 5: Using Structs to Structure Related Data, with defining and instantiating structs (shorthand, update syntax, tuple and unit structs) and an example program.
- The Rust Book, appendix C: Derivable Traits, what each derive generates.
- Rust by Example: Structures and Derive.
- The Rust Reference: Type layout, what is promised about the default layout and
repr(C); andoffset_of!,size_ofandalign_ofinstd::mem. - The Rust Book, chapter 7.2: Control Scope and Privacy with Modules, and the Reference on visibility and privacy.
DebugandDefaultin the standard library docs.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.