Skip to content
Learn Rust
Theme

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

Rustsrc/main.rsRun ↗
#[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:?}");
}
$ cargo run
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)
}
$ go run main.go
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)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
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)
$ python3 main.py
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);
$ tsc --strict --module nodenext --types node main.ts && node main.js
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);
}
}
$ javac Main.java && java Main
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);
$ dotnet run
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;
}
$ gcc -Wall -Wextra main.c -o main && ./main
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 yourself
std::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";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
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 derive Clone, PartialEq (for ==), Default, and more.
  • String::from("ana") makes an owned String from 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 from old. If it takes a String or other owned field, old loses it.
  • Fields are private outside their module unless marked pub. There is no capital-letter rule.
Gotcha

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.
Rustsrc/main.rs✗ won't compile
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)
}
$ go run main.go✓ runs
ana 0 false
Kotlin✗ refuses too

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
@dataclass
class User:
name: str
age: int
active: bool
u = User(name="ana")
print(u.name)
$ python3 main.py! crashes at runtime
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 too

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, active
public 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);
}
}
$ javac Main.java && java Main✓ runs
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; }
}
$ dotnet run✓ runs
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;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
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";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
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.
$ cargo buildWhat is E0063?
error[E0063]: missing fields `active` and `age` in initializer of `User`
 --> src/main.rs:8:13
  |
8 |     let u = User {
  |             ^^^^ missing `active` and `age`
What happens in Go

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.

What happens in Kotlin

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.

What happens in Python

A dataclass won't let you skip fields either. Python raises a TypeError when you build the object, naming the two missing fields.

What happens in TypeScript

TypeScript refuses this too: an object typed as User must have every field, so age and active are reported as missing.

What happens in Java

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.

What happens in C#

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.

What happens in C

C sets the fields you leave out to zero, so this prints ana 0 0. Rust makes you give every field.

What happens in C++

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.

What Rust sees

Rust has no automatic zero values. Every field must be given.

Why Rust says no

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.

Rust, fixedsrc/main.rsRun ↗
#[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);
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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);
}
$ cargo run
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:
Rustsrc/main.rs✗ won't compile
struct User {
name: String,
}
fn main() {
let mut u = User {
name: String::from("ana"),
};
u.nickname = String::from("an");
println!("{}", u.name);
}
$ cargo buildWhat is E0609?
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:

Rustsrc/main.rs✗ won't compile
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);
}
$ cargo buildWhat is E0594?
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:

Rustsrc/main.rsRun ↗
#[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
);
}
$ cargo run
book: Book { title: "Dune", pages: 310, read: false }
Dune has 310 pages, read: false

Rust 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:

Rustsrc/main.rsRun ↗
#[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);
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
#[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:?}");
}
$ cargo buildWhat is E0382?
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:

Rustsrc/main.rsRun ↗
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>());
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
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));
}
$ cargo buildWhat is E0308?
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:

Rustsrc/main.rsRun ↗
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>());
}
$ cargo run
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
Rustsrc/main.rsRun ↗
#[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);
}
$ cargo run
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: false

1.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):

Rustsrc/main.rsRun ↗
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 }));
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
#[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:#?}");
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{} {}", p.x, p.y);
println!("{p:?}");
}
$ cargo buildWhat is E0277?
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.

Rustsrc/main.rsRun ↗
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);
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
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);
}
$ cargo buildWhat is E0382?
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:

Rustsrc/main.rs✗ won't compile
struct Excerpt {
text: &str,
}
fn main() {
let e = Excerpt {
text: "call me Ishmael",
};
println!("{}", e.text);
}
$ cargo buildWhat is E0106?
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:

Rustsrc/main.rs✗ won't compile
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);
}
$ cargo buildWhat is E0616?
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:

Rustsrc/main.rsRun ↗
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));
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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());
}
$ cargo run
customer: ana
same heap bytes: true
id: 7
items: 1
let customer = order.customer;
STACKHEAPorder.customer: String (moved)ptrlen 3cap 3order.items: Vec<String>ptrlen 1cap 1order.id: u327customer: Stringptrlen 3cap 33 bytes of UTF-8ana1 String"tea"
The three fields of 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:

Rustsrc/main.rs✗ won't compile
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));
}
$ cargo buildWhat is E0382?
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 }:

Rustsrc/main.rs✗ won't compile
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);
}
$ cargo buildWhat is E0507?
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:

let u = User { name: String::from("ana"), age: 30, active: true };
STACKHEAPu: User, 32 bytesname: ptrlen 3cap 3age: 30active: truepadding 33 bytes of UTF-8ana
On this compiler 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:

Rustsrc/main.rsRun ↗
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);
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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)
);
}
$ cargo run
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.

The bytes inside one struct (not a stack)
STACKPlain: size 8b: 4 bytesa: 1c: 1padding 2offsets: b 0, a 4, c 5CLayout: size 12a: 1padding 3b: 4 bytesc: 1padding 3offsets: a 0, b 4, c 8
The same three fields. The first row is what rustc 1.99.1 chose for the default layout. The second is #[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.

Starterexercises/settings.rs
#[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
One solutionexercises/settings.solution.rsRun ↗
#[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");
}
$ cargo run
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.

Starterexercises/rects.rs
#[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
One solutionexercises/rects.solution.rsRun ↗
#[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");
}
$ cargo run
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.

Starterexercises/basket.rs
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
One solutionexercises/basket.solution.rsRun ↗
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");
}
$ cargo run
all checks passed

Read more