Introduction to Rust’s Core Concepts
Every language makes a bargain with you about who is responsible for the things your program creates. C’s bargain is that you are responsible for all of it, and the language will not check your work. Java’s bargain is that a garbage collector is responsible for memory and you are responsible for everything else, which turns out to be files, sockets, locks, transactions and the order in which anything happens. Rust’s bargain is different in a way that takes a while to feel natural: responsibility is written into the program, in the types, and the compiler verifies it before the program runs.
This chapter is a tour of what that means. It introduces the ideas the rest of the book develops one at a time, in the order they depend on each other, and it deliberately moves fast. Nothing here is the final word on anything. The point is to get the shape of the model in front of you, so that when a later chapter spends thirty pages on borrowing you already know why borrowing is a thing that needed inventing.
Values and places
Start with a distinction that most languages let you ignore. A place is a
location that can hold something: a local variable, a field of a struct, a slot
in an array. A value is the thing held. In a language where every object lives
on the heap and every variable is a reference to it, the two collapse into each
other and you stop noticing the difference. A Java String s is a place holding
a reference; the value is somewhere else, and how it got there and when it leaves
are not your business.
Rust keeps them apart. When you write
#![allow(unused)]
fn main() {
let greeting = String::from("hello");
}
the variable greeting is a place, and the value in it is a String. That
String is a small struct of three machine words — a pointer to a buffer, a
length, and a capacity — and those three words sit directly in greeting’s
stack slot. The text bytes live in a heap buffer that the String owns. There is
no second layer of indirection, no header, no object identity. A String is a
value that happens to manage some heap memory, and if you put it in a struct, the
three words go inside that struct.
This sounds like an implementation detail and it is not. It is the reason the rest of the model works. Because a value can live directly inside a place, the question of what happens when the place goes away has a real answer, and that answer is the first core concept.
Ownership
Every value in Rust has exactly one owner, and when the owner goes out of scope, the value is destroyed.
One sentence, and most of the rest of the language follows from taking it seriously. Destruction happens at a point in the program you can read off the page, because scopes are lexical and the compiler knows where they end. No collector runs later. No free is written by hand.
You can watch it happen. A type can implement the Drop trait to run code when
it is destroyed:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("dropping {}", self.0);
}
}
fn main() {
let _outer = Noisy("outer");
{
let _inner = Noisy("inner");
println!("inside the block");
}
println!("after the block");
}
The program prints inside the block, then dropping inner, then after the block, then dropping outer. The inner value dies at the closing brace, because
that is where its owner’s scope ends. The outer one dies at the end of main,
after everything else, because owners are dropped in reverse order of
declaration.
Memory is the least interesting thing this schedules. A file handle closes when
the File that owns it is dropped. A lock releases when the guard that owns it
is dropped. A database transaction rolls back when the transaction object is
dropped without having been committed. All of these are the same mechanism, and
none of them needs a finally block, a defer, a with, or a using, because
the cleanup is attached to the value rather than to the block of code that
happened to use it.
Coming from a garbage-collected language
The habit to notice is that you have been thinking of memory as free and everything else as needing ceremony. You close files in
finallyblocks and you have been burned at least once by a connection pool that leaked because some path out of a method skipped the close. Rust inverts this: the ceremony disappears and the schedule becomes something you design. The cost is that you have to decide who owns things, and you have to decide it up front.
Moves
If a value has one owner, then giving it to someone else means giving up being the owner. This is a move, and it is what assignment does:
fn main() {
let a = String::from("hello");
let b = a;
println!("{a}");
}
That program does not compile. The compiler says borrow of moved value: a, and
points out that the String was moved into b on the previous line.
The rule that produces this is not arbitrary strictness. Follow the alternative
through. If both a and b owned the same String, then at the end of the
block both would be dropped, both would free the same heap buffer, and the
program would corrupt its allocator. Languages without ownership avoid that
outcome by never freeing on scope exit — a collector decides later, or nobody
does. Rust avoids it by making the assignment transfer responsibility rather than
duplicate it.
Passing a value to a function moves it for the same reason, and so does returning one. A function signature is therefore a statement about responsibility. When you write
#![allow(unused)]
fn main() {
fn consume(text: String) -> usize {
text.len()
}
}
you are saying that consume takes over the caller’s String and the caller
will not see it again. That is sometimes what you want, and usually it is not,
which brings us to the next idea.
Borrowing
Most of the time a function needs to look at a value, or change it, without
taking responsibility for destroying it. That is a borrow, written with &:
fn length(text: &String) -> usize {
text.len()
}
fn shout(text: &mut String) {
text.make_ascii_uppercase();
}
fn main() {
let mut message = String::from("hello");
println!("{}", length(&message));
shout(&mut message);
println!("{message}");
}
&message is a shared reference: it grants the right to read, and the owner
keeps ownership. &mut message is an exclusive reference: it grants the right to
modify, and while it exists nobody else may touch the value at all, not even to
read it.
The rule governing references is short enough to memorize and deep enough to spend a career on. At any given moment, a value may have any number of shared references or exactly one exclusive reference, and never both. Chapter 3 is about why that single sentence eliminates an entire family of bugs — iterator invalidation, use-after-free, and data races turn out to be the same bug wearing different clothes — and about how to design so that you are not fighting it.
Note
Read
&mutas “exclusive” rather than “mutable”. The important half of the guarantee is not that you may write through it; it is that nobody else can do anything through anything else while you hold it. The name is historical and it misleads nearly every newcomer at least once.
Mutability is a property of the binding, not the data
In an object-oriented language, mutability is a property of an object. Anyone
holding a reference to a mutable object may change it, and the number of people
holding such a reference is not knowable. That is why defensive copying exists,
why immutable classes are recommended, and why a Collections.unmodifiableList
wrapper has to exist as a runtime trick.
In Rust the question is asked one level up. A value does not have a mutable setting; a path to a value does. The owner declares whether it may change the thing it owns:
#![allow(unused)]
fn main() {
let fixed = String::from("hello");
let mut changeable = String::from("hello");
}
and a reference declares whether it carries the right to change:
#![allow(unused)]
fn main() {
let mut changeable = String::from("hello");
let look: &String = &changeable;
println!("{look}");
let change: &mut String = &mut changeable;
change.push('!');
}
The consequence is that shared data is read-only by construction. You never have to wonder who else might write to something you are reading, because if anyone could, you would not have been allowed to read it. Defensive copying stops being a practice.
The type carries what must be true
Rust’s second big idea is that a type is the right place to put facts you do not want to check at runtime, and its enums are strong enough to make that practical. An enum is not a list of named integers. It is a choice between alternatives, each of which may carry its own data:
#![allow(unused)]
fn main() {
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
}
A Shape is one of those three and there is no fourth possibility, no null, and
no subclass someone adds in another crate. The match must cover every variant
or the program does not compile, so adding a Square variant later turns into a
list of compile errors naming every place that needs to think about squares. That
is the opposite of the usual experience, where adding a case means hunting for
the switches that need updating and finding the one you missed in production.
Two enums in the standard library do most of the work in practice. Option<T> is
either Some(value) or None, and it is how Rust says “there might not be one”
without having a null. Result<T, E> is either Ok(value) or Err(error), and
it is how a function that can fail says so in its return type rather than in its
documentation. Neither can be ignored by accident: you cannot use the T without
first dealing with the case where there isn’t one.
Coming from Java, C# or Python
The habit is checking for null, or not checking and finding out later. The replacement is not “check more carefully”. It is to notice that a nullable reference is a type saying two different things at once, and that separating them lets the compiler track which one you are holding. A
Stringin Rust is always a string. If it might not be there, its type says so.
Traits describe capability, not classification
The third idea is that behavior is attached to types separately from the types themselves. A trait is a named set of things a type can do:
use std::fmt;
struct Money {
cents: i64,
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.cents / 100, self.cents % 100)
}
}
fn main() {
println!("{}", Money { cents: 1250 });
}
Money is not a subclass of anything. It gained the ability to be formatted
because an impl block said how, and it could have gained that ability in a
different crate from the one that defined it. A function can then require the
capability rather than a position in a hierarchy: fn log<T: fmt::Display>(item: T) accepts anything that can display itself, including types that did not exist
when log was written.
There is no inheritance in Rust. No struct extends another struct, there are no
base classes, no super, no overriding. This is the part that most unsettles
programmers arriving from an object-oriented background, and chapters 9 and 11
are about why the loss is smaller than it sounds and what replaces it. The short
version is that inheritance bundles four separate things — subtyping, code reuse,
interface conformance and a shared initialization path — and Rust unbundles them,
handing interface conformance to traits, code reuse to composition and generics,
and initialization to ordinary functions.
The compiler is part of the thought process
One practical note before the real chapters begin. Rust’s error messages are unusually good, and treating them as noise to be silenced is the single most expensive mistake a newcomer makes. When the compiler says that something does not live long enough, it is not asking you to add an annotation until it stops complaining. It is telling you that your program contains a reference that outlives the thing it refers to, which is a fact about your design, and which would have been a crash in C and a mystery in a language with a collector.
The productive loop is to read the error as a question about the design. Who should own this. Does this really need a reference, or does it need its own copy. Is this function doing two jobs that want different access. Programmers who run that loop for a few weeks report the same thing: the errors stop appearing, not because they learned the tricks, but because they stopped writing the designs that produce them.
What the model buys
The reward for all of this arrives in chapter 14 and is worth naming now. Because the compiler tracks who owns each value and enforces that a value has either many readers or one writer, it already has everything it needs to prove that two threads cannot race on the same data. Data race freedom is not a separate feature bolted onto Rust’s concurrency story; it is the same rule that prevents use-after-free, applied across threads instead of across time. You get to share data between threads without a mutex when the sharing is read-only, and the compiler will not let you forget the mutex when it is not.
That is the bargain. You answer questions about responsibility that other languages let you leave unanswered, and in exchange a class of bugs that dominates the industry’s crash reports becomes unrepresentable.
Conclusion
Six ideas carry the rest of the book. Values live in places and are owned by exactly one place at a time. Ownership schedules destruction, and destruction handles far more than memory. Giving a value away is a move, because responsibility cannot be duplicated. Borrowing grants access without responsibility, under a rule that permits many readers or one writer. Types, especially enums, are where you put the facts you do not want to check at runtime. Traits name capabilities, and capability is what code should depend on rather than ancestry.
The next chapter slows down and takes the first two seriously, because ownership is the concept everything else is built on and the one that most needs to become reflexive.
Questions to think through
Think about the last program you wrote in another language. Pick one object that lived a long time and was referred to from several places. Who was responsible for deciding it was finished, and how would you have written that down?
The Drop example in this chapter prints its messages in an order determined
entirely by the shape of the source code. What would have to be true about a
language for that to be impossible?
A function signature in Rust distinguishes taking a value, borrowing it, and borrowing it exclusively. Find a method in a codebase you know well and decide which of the three it actually wants. Does its current signature say so?