Variables in Rust are basically immutable. It is a much more conservative starting point than "variables" in other languages, but thanks to it, concurrency and memory safety can be statically guaranteed. In this tutorial, you will learn the basic operation of let, the difference between mut and shadowing, constants (const/static), and type inference and explicit grammar.
Learning Objectives
- Understand that the value grouped with
letis a basic immutable and distinguish it fromlet mut. - Learn how to overwrite a new variable with the same name through shadowing.
- Explains the difference between
constandstaticand the need for an explicit type. - Know how Rust’s type inference works, and specify types where necessary.
Key concepts
let x = 10;
x = 20; // ❌ cannot assign twice to immutable variable `x`
The reason immutability is fundamental is simple. If you start with read-only, later when you read the code you'll wonder "Where did this value change?" No need to keep track. Data races are also reduced.
If the value really needs to change, add mut. However, it is still not possible to assign different types to the same variable.
let mut count = 0;
count += 1; // OK
//count = "0"; // ❌ Type cannot be changed
If you use let name = ...; one more time, a new variable is created with the same name. There are two key differences from mut.
- The type can be changed (e.g.
&str→usize). - Leave the existing variable as is and “overlay” the new variable on top of it — hence shadowing.
4) const and static
| Keyword | Evaluation Point | Specify type | memory | change |
|---|---|---|---|---|
let |
runtime | Inference/Specification | stack/heap | mut Siman |
const |
compile time | Required | Inlining | Not possible |
static |
Start Runtime | Required | fixed address | unsafe required |
By convention, the constant name is SCREAMING_SNAKE_CASE.
For most variables, the type is inferred from the value on the right-hand side. Where the inference is ambiguous, clarification is required.
let parsed: i32 = "42".parse().expect("is not a number."); //The return type of parse is ambiguous.
let pi = 3.14f32; //Can also be specified with the suffix.
See as example
//Part 2 Example 1: let immutable variable and let mut mutable variable
// `cargo run --bin ex01_basics`
fn main() {
//Basic variables are “immutable”. Once a value is bound, it cannot be changed again.
let x = 10;
println!("x = {x}");
//The `mut` keyword is required to reassign to the same variable.
let mut y = 5;
println!("First y = {y}");
y = y + 1;
println!("After increase y = {y}");
//The type can also be specified. (If omitted, Rust infers)
let pi: f64 = 3.14;
let flag: bool = true;
println!("pi = {pi}, flag = {flag}");
}
//Part 2 Example 2: Shadowing — Overwriting a new variable with the same name
// `cargo run --bin ex02_shadowing`
fn main() {
//Shadowing is regrouping the same name with `let`.
//Unlike `mut`, the type can also be changed, resulting in a "new variable".
let value = 5;
let value = value + 1; // 6
let value = value * 2; // 12
println!("value = {value}");
//Shadowing that changes the type
let spaces = " "; // &str
let spaces = spaces.len(); // usize
println!("Space length = {spaces}");
//Not possible with mut: Other Compilation error occurs when assigning type.
// let mut s = " "; s = s.len(); // ❌
}
//Part 2 Example 3: const and static
// `cargo run --bin ex03_consts`
//const is determined at compile time and must specify the type.
const MAX_USERS: u32 = 100_000;
//static exists in the same memory throughout the life of the program.
static APP_NAME: &str = "Lecture_Rust";
fn main() {
println!("APP_NAME = {APP_NAME}");
println!("MAX_USERS = {MAX_USERS}");
//`_` in a numeric literal is a readability separator (has no effect on the value)
let big = 1_000_000;
println!("big = {big}");
//Type suffix — 23i64, Can be specified as 1.5f32
let count = 23i64;
let ratio = 1.5f32;
println!("count = {count}, ratio = {ratio}");
}
Common Mistakes
A. Shadowing is useful for naturally expressing type-changing conversion. For example, when converting an input string to a number, you can only change the form with the same name, such as let input: String = ...; → let input: i32 = input.parse()...;. mut is used when updating only the value of the same variable.
A. The value of const must be confirmed at compile time and the type must be specified (e.g. const PI: f64 = 3.14;). let can also receive runtime values, and the type is usually inferred. If possible, leave what appears to be a function internal constant as const to make the intention clear.
A. This is a separator for readability. It is exactly the same value as 1000000, and the position is free (same as 1_00_00_00). It is often used for large numbers or hexadecimal numbers (0xFF_FF).
A. Usually, the compiler infers it even if you don't specify it. However, if the return type is general like .parse(), you must write the type in the variable or use TurboFish syntax like parse::<i32>().
Cleanup
- The basic variable is specified as immutable, and variable is specified as
mut. - Shadowing allows you to create a new variable with the same name and change its type.
constis a compile-time constant, andstaticis a static variable that lives throughout the life of the process.- Type inference is strong, but explicit when ambiguous (
let x: i32 = ...;or Turbofish).
Try it yourself
Solve the assignment below. The reference solution is shown below.