Skip to content

Rust - Variables

A variable is a named storage location that programs can manipulate. In Rust, variables are not just about storing data; they are deeply integrated with the language’s core principles of safety and ownership.

You declare a variable using the let keyword. Rust is a statically typed language, which means it must know the type of all variables at compile time. However, the compiler is smart and can usually infer the type based on the value you assign.

fn main() {
// The compiler infers the type of `x` as i32 (integer)
let x = 5;
// Here, we explicitly annotate the type as a 64-bit float
let y: f64 = 3.14;
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}

Variable names in Rust follow the snake_case convention, where all letters are lowercase and words are separated by underscores. They can contain letters, numbers, and underscores, but must start with a letter or an underscore.

One of Rust’s key safety features is that variables are immutable by default. Once a value is bound to a name, you cannot change that value.

fn main() {
let x = 5;
println!("The value of x is: {}", x);
// The following line will cause a compile-time error!
// x = 6; // error[E0384]: cannot assign twice to immutable variable `x`
// println!("The value of x is now: {}", x);
}

This compile-time check prevents a whole class of bugs where a value is accidentally changed. When you know a value won’t change, it’s easier to reason about your code.

Of course, sometimes you need to change a variable’s value. To make a variable mutable, you must explicitly opt-in by using the mut keyword.

fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is now: {}", x);
}
The value of x is: 5
The value of x is now: 6

Using mut signals to other developers (and to yourself) that this variable’s value is expected to change during its lifetime.

Rust allows you to declare a new variable with the same name as a previous variable. This is called shadowing. The new variable “shadows” the old one, and the old variable is inaccessible from that point on in the scope.

fn main() {
let x = 5;
// a new `x` is created, shadowing the first one.
let x = x + 1;
{
// in this inner scope, a third `x` is created, shadowing the second one.
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);
}
// when the inner scope ends, the third `x` is gone, and we're back to the second one.
println!("The value of x is: {}", x);
}
The value of x in the inner scope is: 12
The value of x is: 6

Shadowing is different from marking a variable as mut. A key benefit of shadowing is that it allows you to change a variable’s type while reusing its name, which is often useful for conversions.

fn main() {
let spaces = " "; // `spaces` is a string slice
let spaces = spaces.len(); // `spaces` is now shadowed by a new variable of type `usize`
println!("There are {} spaces.", spaces);
}

This is allowed because we are creating a completely new variable with let. If we tried to do this with mut, it would fail because you cannot change a variable’s type.