Variables
Basics · Lesson 8
Variables are declared with let. They cannot change unless you add mut.
Coming from Golet is your :=, but the variable can't change unless you write let mut. Writing let count = ... again in the same scope is allowed and makes a new variable, where Go's := would complain that nothing is new.
Coming from Kotlinlet is your val and let mut is your var, and let also freezes the contents: a val list still takes add, a Rust let list does not take push. The other difference is shadowing: you can write let count = ... again in the same scope, even with a new type, which Kotlin won't allow.
Coming from PythonAny Python name can be reassigned, but a Rust let can't change unless you write let mut. Writing let count = ... again makes a new variable, and its type is still checked at compile time.
Coming from TypeScriptRust's let is your const and let mut is your let. Unlike const, a plain let also stops you changing what's inside (pushing to a Vec, setting a field), and you may redeclare the same name in one scope. (References and Cell-style types are the exceptions; the page covers them.)
Coming from JavaRust flips Java's default: let is final var, and you add mut to get a plain changeable local. Rust's let also freezes the contents, where a final list still takes add. Rust also lets you shadow a name with a new let, which Java forbids for locals.
Coming from C#let is a local you cannot reassign, which C# has no keyword for (const only works for compile-time constants, and locals can't be readonly). let mut is your everyday var, and a second let with the same name shadows the first, which C# rejects.
Coming from Clet is const int, and let mut is a plain int, so the default is the reverse of C. Shadowing is allowed even in the same block, where C would report a redeclaration.
Coming from C++let x is like const auto x, and let mut x like plain auto x, so Rust makes const the default. The difference: auto reads only the right-hand side, and let can wait for a later line to decide the type. Shadowing with a second let is allowed in the same scope, which C++ only allows in an inner block.
The quick comparison
fn main() { let name = "gopher"; // cannot change let mut count = 1; // mut: can change count += 1;
let count = count * 10; // shadowing: a NEW variable with the same name println!("{name} {count}");}gopher 20
package main
import "fmt"
func main() { name := "gopher" count := 1 count += 1 count = count * 10 fmt.Println(name, count)}gopher 20
fun main() { val name = "gopher" var count = 1 count += 1 count = count * 10 println("$name $count")}gopher 20
name = "gopher"count = 1count += 1
count = count * 10 # Python just reassigns; there is no separate "shadowing"print(f"{name} {count}")gopher 20
const mascot = "gopher"; // const: cannot be reassignedlet count = 1; // let: can changecount += 1;
// TypeScript has no shadowing in the same scope, so use a new nameconst scaled = count * 10;console.log(`${mascot} ${scaled}`);gopher 20
public class Main { public static void main(String[] args) { final var name = "gopher"; // final: cannot change var count = 1; // locals can change by default count += 1;
count = count * 10; // a local cannot reuse a local's name in Java: reassign instead System.out.println(name + " " + count); }}gopher 20
const string name = "gopher"; // cannot changevar count = 1; // var: can changecount += 1;
// C# does not let a local reuse a local's name, so the new value gets a new namevar total = count * 10;Console.WriteLine($"{name} {total}");gopher 20
#include <stdio.h>
int main(void) { const char *const name = "gopher"; // neither the pointer nor the text can change int count = 1; // C variables can change by default count += 1;
count = count * 10; // C has no shadowing in the same block: reuse it printf("%s %d\n", name, count); return 0;}gopher 20
#include <iostream>#include <string>
int main() { const std::string name = "gopher"; // const: cannot change int count = 1; // plain variables can change count += 1;
const int total = count * 10; // no shadowing in the same scope: pick a new name std::cout << name << " " << total << "\n";}gopher 20
What's different
- Plain
letcan't change. You opt in withlet mut, so a reader can see at a glance which variables change. - Shadowing (
let count = ...again) makes a new variable that hides the old one. It can even have a different type. It is common for steps likelet input = input.trim();. constis for a value known before the program runs. It always needs a type and may sit outside any function, and by convention its name isSHOUTING_CASE.- A variable you never read gets a compiler warning. Start its name with
_to say that is on purpose.
Changing a variable without mut
Looks fine: in Go, Java, C#, C and C++ it runs fine. Rust won't compile it.
Works in Go. In Rust it's just a rule.Kotlin refuses this too. Same idea, different words.No direct equivalent in Python. Just a Rust rule.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.fn main() { let count = 1; count += 1; println!("{count}");}package main
import "fmt"
func main() { count := 1 count += 1 fmt.Println(count)}2
Kotlin refuses this too if you write val count = 1; with var it runs and prints 2. Rust's let is Kotlin's val, and let mut is var. What is new is that Rust's let also stops the push on a list, as the page shows.
fun main() { val count = 1 count += 1 println(count)}Main.kt:3:5: error: 'val' cannot be reassigned.
count += 1
^^^^^Python has no mut: every variable can be reassigned, so this mistake can't happen.
TypeScript refuses this too if you write const: you cannot assign to a constant. Rust's let is TypeScript's const, and let mut is let.
const count = 1;count += 1;console.log(count);main.ts(2,1): error TS2588: Cannot assign to 'count' because it is a constant.
public class Main { public static void main(String[] args) { var count = 1; count += 1; System.out.println(count); }}2
var count = 1;count += 1;Console.WriteLine(count);2
#include <stdio.h>
int main(void) { int count = 1; count += 1; printf("%d\n", count); return 0;}2
#include <iostream>
int main() { int count = 1; count += 1; std::cout << count << "\n";}2
What does the compiler say? Guess first, then open.
error[E0384]: cannot assign twice to immutable variable `count` --> src/main.rs:3:5 | 2 | let count = 1; | ----- first assignment to `count` 3 | count += 1; | ^^^^^^^^^^ cannot assign twice to immutable variable | help: consider making this binding mutable | 2 | let mut count = 1; | +++
Go runs this and prints 2. A variable made with := can always be changed later.
Kotlin refuses this too if you write val count = 1; with var it runs and prints 2. Rust's let is Kotlin's val, and let mut is var. What is new is that Rust's let also stops the push on a list, as the page shows.
Python has no mut: every variable can be reassigned, so this mistake can't happen.
TypeScript refuses this too if you write const: you cannot assign to a constant. Rust's let is TypeScript's const, and let mut is let.
Java locals can change by default, so this prints 2. Write final var count = 1 and javac refuses it, like Rust's let.
A plain var can always change in C#, so this prints 2. Only const and readonly fields are locked, and local variables can't be readonly.
C variables can change unless you mark them const, so this prints 2. Rust flips the default: you opt in to change with mut.
C++ variables can change unless you mark them const, so this prints 2. Rust flips the default: you opt in to change with mut.
In Rust, plain let means "this never changes". You only get a changeable variable when you ask for one with mut.
Rust chose this default so that a reader can see which variables change. The Rust Book's reason: code that assumes a value never changes can break when other code changes it. When you read let x, that name is not assigned again; only the mut ones need watching.
The fix
Compiles and runs.
fn main() { let mut count = 1; count += 1; println!("{count}");}2
The toolkit
The Syntax part showed how let, const and static are spelled. This lesson is about what they do. Outputs come from Linux on 64-bit ARM with Rust 1.99.1.
Frozen unless you say mut
A let variable cannot be assigned to once it has a value (E0384, in the gotcha above); mut lifts that. It also freezes the value the variable owns.
fn main() { let list = Vec::new(); list.push(1); println!("len: {}", list.len());}error[E0596]: cannot borrow `list` as mutable, as it is not declared as mutable --> src/main.rs:3:5 | 3 | list.push(1); | ^^^^ cannot borrow as mutable | help: consider changing this to be mutable | 2 | let mut list = Vec::new(); | +++
E0596. push changes the list, and its variable is not mut. The message says “borrow as mutable”: a method that changes a value borrows it mutably (Borrowing; Vec is a growable list, Vectors). References and Cell-style types are the exceptions (Going deeper).
Why is this the default? The Rust Book’s reason: if one part of the code assumes a value never changes and another changes it, the first part may misbehave. With plain let a reader need not check; only let mut variables need watching.
Shadowing or mut?
Writing let again with the same name makes a new variable and hides the old one (spelling). The two tools do different jobs:
let mut x then x = ... |
let x = ... again |
|
|---|---|---|
| Variable | the same one | a new one |
| Type | must stay the same | may change |
| Inside a block | the change outlives the block | the new variable ends with the block |
| Afterwards | still mut |
frozen again, unless you write let mut |
fn main() { let mut a = 1; let b = 1; println!("before: a = {a}, b = {b}"); { a = 10; // changes the one variable let b = 10; // a new variable, gone when the block ends println!("inside: a = {a}, b = {b}"); } println!("after: a = {a}, b = {b}");}before: a = 1, b = 1 inside: a = 10, b = 10 after: a = 10, b = 1
a was changed for good. b was hidden inside the block and came back after it. A new variable can have a new type (these two sizes are two variables):
fn main() { let speed = 90; // i32 println!("first: {} bytes", size_of_val(&speed)); let speed = speed as f64 * 1.6; // a new variable: f64 println!("second: {} bytes, {speed}", size_of_val(&speed));}first: 4 bytes second: 8 bytes, 144
Assigning a different type to a mut variable is an error:
fn main() { let mut speed = 90; speed = speed as f64 * 1.6; println!("{speed}");}error[E0308]: mismatched types --> src/main.rs:3:13 | 2 | let mut speed = 90; | -- expected due to this value 3 | speed = speed as f64 * 1.6; | ^^^^^^^^^^^^^^^^^^ expected integer, found `f64`
Use mut for something that really changes, such as a counter, and shadowing for a step that makes a new version of the same thing (let input = input.trim();, a conversion).
const, static and let
let |
const |
static |
|
|---|---|---|---|
| Value | any expression, computed when the line runs | a constant expression, computed before the program runs | a constant expression |
| Can change | only with mut |
never | not by assignment (atomic types and Mutex can change), and static mut needs unsafe for every use |
| In memory | a variable | copied into each place that uses it | one place; all references point to it |
The spelling, where each may sit are on Declarations and types, and the naming is under Naming. The “In memory” row is the Rust Reference on constants and statics. The programs below run the other rows (static mut is in what Rust lacks).
const SECONDS_PER_DAY: u32 = 24 * 60 * 60;static APP_NAME: &str = "tracker";
fn main() { const LIMIT: u32 = 3; // a const may sit inside a function let days = 2; let seconds = days * SECONDS_PER_DAY; println!("{APP_NAME}: {seconds} seconds in {days} days (limit {LIMIT})");}tracker: 172800 seconds in 2 days (limit 3)
A const cannot use a runtime value, even one that looks fixed:
fn main() { let n = 3; const LIMIT: i32 = n * 2; println!("{LIMIT}");}error[E0435]: attempt to use a non-constant value in a constant --> src/main.rs:3:24 | 3 | const LIMIT: i32 = n * 2; | ^ non-constant value | help: consider using `let` instead of `const` | 3 - const LIMIT: i32 = n * 2; 3 + let LIMIT: i32 = n * 2;
E0435. The help: suggests let, which is what you want for a value that is not known in advance. Nor can a static be assigned to:
static COUNT: u32 = 0;
fn main() { COUNT += 1; println!("{COUNT}");}error[E0594]: cannot assign to immutable static item `COUNT` --> src/main.rs:4:5 | 1 | static COUNT: u32 = 0; | ----------------- this `static` cannot be written to ... 4 | COUNT += 1; | ^^^^^^^^^^ cannot assign
A static can sit inside a function and keep its value between calls. It changes through a type made for that, here AtomicU32 (Shared state):
use std::sync::atomic::{AtomicU32, Ordering};
fn next() -> u32 { static CALLS: AtomicU32 = AtomicU32::new(0); // a static inside a function CALLS.fetch_add(1, Ordering::Relaxed) + 1 // changes without `mut` or `unsafe`}
fn main() { println!("calls: {} {} {}", next(), next(), next());}calls: 1 2 3
Reach for const first; use a static when the value should live in one place for the whole program. Mutable global state is awkward on purpose: see what Rust lacks.
Variables you do not use
An unused variable and a mut that is never needed are warnings, not errors: the lints unused_variables and unused_mut are on by default. #![deny(unused)] (lint levels) turns them into errors for the whole file:
#![deny(unused)]
fn main() { let mut total = 10; let scratch = 5; println!("total: {total}");}error: variable does not need to be mutable --> src/main.rs:4:9 | 4 | let mut total = 10; | ----^^^^^ | | | help: remove this `mut` | note: the lint level is defined here --> src/main.rs:1:9 | 1 | #![deny(unused)] | ^^^^^^ = note: `#[deny(unused_mut)]` implied by `#[deny(unused)]` error: unused variable: `scratch` --> src/main.rs:5:9 | 5 | let scratch = 5; | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_scratch` | = note: `#[deny(unused_variables)]` implied by `#[deny(unused)]`
For “unused on purpose”, start the name with an underscore. _kept is still a variable; _ alone binds nothing:
fn main() { let _scratch = 5; // never read: no warning let _ = 6; // binds nothing let _kept = 7; // still a real variable println!("kept: {_kept}");}kept: 7
Declared now, set later
A let without a value is allowed (the working form), but a read that some path reaches unset is E0381:
fn main() { let label; let n = 3; if n > 2 { label = "big"; } println!("label: {label}");}error[E0381]: used binding `label` is possibly-uninitialized --> src/main.rs:7:23 | 2 | let label; | ----- binding declared here but left uninitialized 3 | let n = 3; 4 | if n > 2 { | ----- if this `if` condition is `false`, `label` is not initialized 5 | label = "big"; 6 | } | - an `else` arm might be missing here, initializing `label` 7 | println!("label: {label}"); | ^^^^^ `label` used here but it is possibly-uninitialized | = note: when checking initialization, the compiler describes possible control-flow paths without evaluating whether branch conditions can actually have the values shown
A plain let takes its value once; a second assignment is E0384 again:
fn main() { let label; label = "first"; label = "second"; println!("label: {label}");}error[E0384]: cannot assign twice to immutable variable `label` --> src/main.rs:4:5 | 3 | label = "first"; | --------------- first assignment to `label` 4 | label = "second"; | ^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable | help: consider making this binding mutable | 2 | let mut label; | +++
Inference looks ahead
A later use can decide a variable’s type: let a = 5; becomes a u8 if a takes_u8(a) call follows (the type is often optional). If two uses disagree, the error is at the second:
fn takes_u8(n: u8) -> u8 { n}
fn takes_u64(n: u64) -> u64 { n}
fn main() { let a = 5; takes_u8(a); takes_u64(a);}error[E0308]: mismatched types --> src/main.rs:12:15 | 12 | takes_u64(a); | --------- ^ expected `u64`, found `u8` | | | arguments to this function are incorrect | note: function defined here --> src/main.rs:5:4 | 5 | fn takes_u64(n: u64) -> u64 { | ^^^^^^^^^ ------ help: you can convert a `u8` to a `u64` | 12 | takes_u64(a.into()); | +++++++
Your language, side by side
Going deeper
mut belongs to the name, not the value
Whether a value can change depends on its variable: move it into a mut variable and it can change (“moved” is Ownership):
fn main() { let first = String::from("ab"); let mut second = first; // the text moves into a variable that may change second.push('c'); println!("second: {second}");}second: abc
The reverse works too. Shadowing a mut variable with a plain let freezes it again, a way to say “built, now finished”:
fn main() { let mut list = Vec::new(); list.push(1); list.push(2); let list = list; // the same list, now frozen list.push(3);}error[E0596]: cannot borrow `list` as mutable, as it is not declared as mutable --> src/main.rs:6:5 | 6 | list.push(3); | ^^^^ cannot borrow as mutable | help: consider changing this to be mutable | 5 | let mut list = list; // the same list, now frozen | +++
Two things still change a value through a variable that is not mut. A &mut reference changes what it points at (Borrowing):
fn main() { let mut v = vec![1]; let r = &mut v; // `r` is not `mut`, but it points at a value that is r.push(2); println!("v: {v:?}");}v: [1, 2]
And types such as Cell are built to change through a plain let (Smart pointers covers them):
use std::cell::Cell;
fn main() { let counter = Cell::new(0); // no `mut` counter.set(counter.get() + 1); println!("counter: {}", counter.get());}counter: 1
What a const may contain
A const is computed before the program runs, so it can call only const fn functions:
const fn square(n: u32) -> u32 { n * n}
const AREA: u32 = square(4);
fn main() { println!("area: {AREA}");}area: 16
String::from is not one, so this fails with E0015:
const NAME: String = String::from("tracker");
fn main() { println!("{NAME}");}error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants --> src/main.rs:1:22 | 1 | const NAME: String = String::from("tracker"); | ^^^^^^^^^^^^^^^^^^^^^^^ | = note: calls in constants are limited to constant functions, tuple structs and tuple variants
For a fixed piece of text, use &str (const NAME: &str = "tracker";).
Shadowing can hide a mistake
Shadowing inside a block does not touch the outer variable, and the compiler cannot know you meant to update it. The program below compiles and prints total: 0:
#![allow(unused)] // warnings off, to show what the program does
fn main() { let mut total = 0; for n in 1..=3 { let total = total + n; // meant: total += n } println!("total: {total}");}total: 0
That file switches the warnings off. With them on, the compiler notices the inner total is never used and the outer mut never needed:
#![deny(unused)]
fn main() { let mut total = 0; for n in 1..=3 { let total = total + n; // meant: total += n } println!("total: {total}");}error: variable does not need to be mutable --> src/main.rs:4:9 | 4 | let mut total = 0; | ----^^^^^ | | | help: remove this `mut` | note: the lint level is defined here --> src/main.rs:1:9 | 1 | #![deny(unused)] | ^^^^^^ = note: `#[deny(unused_mut)]` implied by `#[deny(unused)]` error: unused variable: `total` --> src/main.rs:6:13 | 6 | let total = total + n; // meant: total += n | ^^^^^ help: if this is intentional, prefix it with an underscore: `_total` | = note: `#[deny(unused_variables)]` implied by `#[deny(unused)]`
Read warnings about unused variables. Here one is the bug.
Practice
Try it
Open each exercise in the Playground, replace the todo!(), and run it until it prints all checks passed.
Fix a counter
This counts the multiples of step from 1 to limit, and the answer is wrong. The compiler already warns about the cause: read its warnings.
/// How many numbers from 1 to `limit` (inclusive) are multiples of `step`?/// A `step` of 0 has no multiples, so the answer is 0./// This version compiles but gives the wrong answer. Read the compiler's warnings: they point at the bug.fn count_multiples(limit: u32, step: u32) -> u32 { if step == 0 { return 0; } let mut count = 0; for n in 1..=limit { if n % step == 0 { let count = count + 1; } } count}
fn main() { assert_eq!(count_multiples(10, 3), 3); // 3, 6, 9 assert_eq!(count_multiples(10, 1), 10); assert_eq!(count_multiples(2, 5), 0); assert_eq!(count_multiples(0, 3), 0); // The case to watch: a step of 0 must not crash. assert_eq!(count_multiples(10, 0), 0); 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
/// How many numbers from 1 to `limit` (inclusive) are multiples of `step`?/// A `step` of 0 has no multiples, so the answer is 0.fn count_multiples(limit: u32, step: u32) -> u32 { if step == 0 { return 0; } let mut count = 0; for n in 1..=limit { if n % step == 0 { count += 1; } } count}
fn main() { assert_eq!(count_multiples(10, 3), 3); // 3, 6, 9 assert_eq!(count_multiples(10, 1), 10); assert_eq!(count_multiples(2, 5), 0); assert_eq!(count_multiples(0, 3), 0); // The case to watch: a step of 0 must not crash. assert_eq!(count_multiples(10, 0), 0); println!("all checks passed");}all checks passed
Shadow to convert
Return the average of total over count as an f64. Shadow each parameter with a converted copy (let total = total as f64;) and divide. No items must give 0.0, not NaN.
/// The average of `total` spread over `count` items, as a float./// With no items the average is 0.0.fn average(total: i32, count: i32) -> f64 { todo!( "shadow both parameters with `let total = total as f64;` (and the same for count), then divide. Check count == 0 first: 0.0 / 0.0 is NaN" )}
fn main() { assert_eq!(average(7, 2), 3.5); assert_eq!(average(10, 5), 2.0); assert_eq!(average(-3, 2), -1.5); // The case to watch: no items must give 0.0, not NaN. assert_eq!(average(0, 0), 0.0); assert_eq!(average(5, 0), 0.0); 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
/// The average of `total` spread over `count` items, as a float./// With no items the average is 0.0.fn average(total: i32, count: i32) -> f64 { if count == 0 { return 0.0; } let total = total as f64; let count = count as f64; total / count}
fn main() { assert_eq!(average(7, 2), 3.5); assert_eq!(average(10, 5), 2.0); assert_eq!(average(-3, 2), -1.5); // The case to watch: no items must give 0.0, not NaN. assert_eq!(average(0, 0), 0.0); assert_eq!(average(5, 0), 0.0); println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 3.1: Variables and Mutability: the reasoning for immutable defaults, shadowing and constants.
- Rust by Example: Variable Bindings, scope and shadowing and constants.
- The Rust Reference: constant items, static items and constant evaluation.
Cellin the standard library docs.- Reading compiler errors: E0384, E0596, E0435, E0381, E0308, E0015 and E0594 from this page.
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.