Variables and Mutability
Variables in Rust look familiar at first: let x = 5;. The difference is the
default. A Rust variable cannot change unless you say so, and the compiler holds
you to it. This lesson covers declaring variables, making them mutable,
shadowing a name with a new value, and the const items you use for values
that are fixed for the whole program.
Declaring variables with let
let binds a value to a name:
fn main() {
let planet = "Mars";
let moons = 2;
println!("{planet} has {moons} moons");
}Mars has 2 moonsYou didn't write any types, but every variable still has one. The compiler
infers them from the values: planet is a string slice (&str) and moons is
an integer (i32, the default integer type). When you want a specific type,
or when the compiler can't work it out, add an annotation after the name:
fn main() {
let moons: u32 = 2;
let gravity: f64 = 3.72;
let habitable: bool = false;
println!("{moons} {gravity} {habitable}");
}2 3.72 falseu32 is an unsigned 32-bit integer, f64 a 64-bit float and bool a
boolean. The next lesson goes through the primitive types in detail.
Variable names use snake_case: lowercase words separated by underscores, like
max_speed or user_count. The compiler warns you if you use another style.
Immutable by default
Once a variable has a value, that value can't be changed:
fn main() {
let score = 10;
println!("score: {score}");
score = 20;
println!("score: {score}");
}This is deliberate. When you read let score = 10;, you know score is 10 for
as long as it exists, without checking every line below it. Most values in a
program never need to change, so Rust makes that the default and makes change
the thing you have to ask for. It also matters later: the borrow checker, which
lets Rust share data safely between parts of a program, relies on knowing what
can change and what can't.
Opting into change with mut
Put mut after let to make a variable mutable:
fn main() {
let mut score = 10;
println!("score: {score}");
score = 20;
score += 5;
println!("score: {score}");
}score: 10
score: 25+= (and -=, *=, /=) update a mutable variable in place.
mut is needed whenever the value itself is modified, not only for =. Calling
a method that changes a value, such as push_str on a String, needs a
mutable variable too:
fn main() {
let mut greeting = String::from("Hello");
greeting.push_str(", Ferris");
greeting.push('!');
println!("{greeting}");
}Hello, Ferris!String is Rust's growable, owned string type; String::from makes one from a
string literal. You will meet it properly in the ownership lesson.
The opposite also holds: if you mark a variable mut and never change it, the
compiler warns you, because the mut is now misleading:
warning: variable does not need to be mutable
--> src/main.rs:2:9
|
2 | let mut lives = 3;
| ----^^^^^
| |
| help: remove this `mut`Declaring now, assigning later
A let doesn't have to give a value straight away. You can declare a variable
and assign it exactly once later, as long as the compiler can see that every
path assigns it before it is read:
fn main() {
let hour = 14;
let greeting;
if hour < 12 {
greeting = "Good morning";
} else {
greeting = "Good afternoon";
}
println!("{greeting}");
}Good afternoongreeting is not mut: it is assigned once on each path. In practice you will
more often write this with if as an expression, which the control flow lesson
covers.
Shadowing
You can declare a new variable with the same name as an existing one. The new one shadows the old: from that line on, the name refers to the new value.
fn main() {
let temperature = 21;
let temperature = temperature * 9 / 5 + 32;
println!("{temperature}°F");
let input = " 42 ";
let input = input.trim();
let input: i32 = input.parse().unwrap();
println!("{}", input + 1);
}69°F
43Each let creates a brand new variable; nothing is mutated. That gives
shadowing two uses that mut doesn't have:
- Transforming a value step by step while keeping the name, and leaving the
result immutable. After the second line,
temperaturecan't be changed by accident. - Changing the type.
inputstarts as a string with spaces, becomes a trimmed string, then becomes ani32. Withmutyou would need three names, such asinput,trimmedandnumber. (parseandunwrapare covered in the lesson onOptionandResult.)
Shadowing also follows scope. A variable declared inside a block { ... } only
lives until the end of that block, and the outer one comes back afterwards:
fn main() {
let level = 1;
{
let level = level + 10;
println!("inner level: {level}");
}
println!("outer level: {level}");
}inner level: 11
outer level: 1Shadowing or mut?
Use mut when a value really changes over time: a counter, a running total, a
buffer you keep appending to. Use shadowing when you are deriving a new value
from an old one and the old one is no longer interesting.
Constants
A constant is a value that is fixed for the whole program. Declare it with
const:
const SECONDS_PER_MINUTE: u32 = 60;
const SECONDS_PER_HOUR: u32 = 60 * SECONDS_PER_MINUTE;
fn main() {
const GREETING: &str = "Hi";
let hours = 3;
let seconds = hours * SECONDS_PER_HOUR;
println!("{GREETING}! {hours} hours is {seconds} seconds");
}Hi! 3 hours is 10800 secondsConstants differ from let variables in a few ways:
- The type is always written out. There is no inference for
const. - They can never be mutable. There is no
const mut. - The value must be known at compile time. Literals, arithmetic on other constants and a few other things are fine; a value computed while the program runs is not.
- They can live anywhere, including outside any function. Constants at the
top of a file are visible to every function in it, which is where most of
them go. A constant declared inside a function, like
GREETING, is only visible there. - They are named in
SCREAMING_SNAKE_CASE.
Add pub in front (pub const ...) to make a constant usable from other
modules, just like a function.
The compiler copies a constant's value into every place it is used, so a
constant has no single address in memory. When you need one global value that
lives at a fixed location, Rust has static, which you will rarely need at
this stage.
Common mistakes
Assigning to a variable without mut
You saw this one above: cannot assign twice to immutable variable (E0384).
Either add mut, or, if you are deriving a new value, shadow the name with
another let.
Changing the type of a mut variable
mut lets the value change, but never the type. This tries to store a number
in a variable that holds a string:
fn main() {
let mut input = " 42 ";
input = input.trim().len();
println!("{input}");
}The fix is shadowing: let input = input.trim().len(); creates a new variable
with the new type.
Reading a variable before it has a value
A variable declared without a value can't be read until it is assigned:
fn main() {
let total: i32;
println!("{total}");
}Rust has no default "zero" or "null" value for variables. Give the variable a value, or make sure every path assigns one before it is used.
Leaving the type off a constant
const MAX_PLAYERS = 4;
fn main() {
println!("{MAX_PLAYERS}");
}Constants always need an annotation: const MAX_PLAYERS: u32 = 4;.
Using a runtime value in a constant
A const can't be built from a let variable, because the variable's value
only exists once the program runs:
error[E0435]: attempt to use a non-constant value in a constant
--> src/main.rs:3:30
|
3 | const MAX_PLAYERS: u32 = players * 2;
| ^^^^^^^ non-constant valueIf the value depends on something computed at runtime, it is a let variable,
not a constant.
Unused variables
A variable you never read triggers a warning. If you really do want to keep it, for example while a function is half written, start its name with an underscore:
fn main() {
let _unused = 5;
}Summary
letdeclares a variable. Types are inferred, or written aslet x: u32.- Variables are immutable by default. Add
mutto change the value, including through methods likepush_str. - A variable can be declared first and assigned later, once on every path, before it is read.
- Shadowing (
let x = ...again) creates a new variable with the same name. It can change the type and keeps the result immutable; it ends with the block it is declared in. constvalues are always typed, never mutable, known at compile time and named inSCREAMING_SNAKE_CASE. They can be declared at the top level or inside a function.
Practise what you read
Challenges that use the ideas from this lesson.