Variables & Mutability
Rust takes a unique approach to variables and mutability that helps prevent common programming errors while maintaining performance. By default, variables in Rust are immutable, but you can opt into mutability when needed.
Immutable Variables by Default
In Rust, variables are immutable by default, meaning once you assign a value, you cannot change it:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
// x = 6; // This would cause a compile error
// error[E0384]: cannot assign twice to immutable variable `x`
}
This immutability-by-default approach helps prevent bugs and makes code easier to reason about, especially in concurrent programs.
Making Variables Mutable
When you need to change a variable's value, you can make it mutable using the mut
keyword:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6; // This is now allowed
println!("The value of x is: {}", x);
}
When to Use Mutability
Use mutable variables when:
- You need to modify the value after initialization
- Building up data structures incrementally
- Performance requires in-place modification
- Working with iterative algorithms
fn main() {
let mut numbers = Vec::new();
// Building up a vector
numbers.push(1);
numbers.push(2);
numbers.push(3);
println!("Numbers: {:?}", numbers);
// Modifying existing values
let mut counter = 0;
for i in 1..=10 {
counter += i;
}
println!("Sum: {}", counter);
}
Constants
Constants are always immutable and are declared using the const
keyword:
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159;
fn main() {
println!("Maximum points: {}", MAX_POINTS);
println!("Pi: {}", PI);
}
Constants vs Immutable Variables
Constants | Immutable Variables |
---|---|
Always immutable | Immutable by default |
Must be const | Use let |
Type must be annotated | Type can be inferred |
Can only be set to constant expressions | Can be set to runtime values |
Valid for entire program run | Valid within their scope |
const SECONDS_IN_HOUR: u32 = 60 * 60; // Constant expression - OK
fn main() {
let current_time = std::time::SystemTime::now(); // Runtime value - OK for let
// const NOW: std::time::SystemTime = std::time::SystemTime::now(); // Error!
}
Variable Shadowing
Rust allows you to declare a new variable with the same name as a previous variable:
fn main() {
let x = 5;
let x = x + 1; // Shadows the previous x
let x = x * 2; // Shadows again
println!("The value of x is: {}", x); // 12
}
Shadowing vs Mutability
Shadowing is different from making a variable mutable:
fn main() {
// Shadowing allows type changes
let spaces = " "; // string
let spaces = spaces.len(); // number
// Mutability doesn't allow type changes
let mut spaces_mut = " ";
// spaces_mut = spaces_mut.len(); // Error: mismatched types
}
Practical Shadowing Examples
Shadowing is useful for transforming values:
fn main() {
// Reading and parsing user input
let input = "42";
let input: i32 = input.trim().parse().expect("Not a number");
// Transforming data through multiple steps
let data = "hello world";
let data = data.to_uppercase();
let data = data.replace(" ", "_");
println!("Transformed data: {}", data); // HELLO_WORLD
}
Scope and Lifetime
Variables are valid within the scope where they're declared:
fn main() {
let outer = 10;
{
let inner = 20;
println!("Inner scope: {} {}", outer, inner); // Both accessible
let outer = 30; // Shadows the outer variable within this scope
println!("Shadowed outer: {}", outer); // 30
} // inner goes out of scope here
println!("Back to outer scope: {}", outer); // 10 (original value)
// println!("{}", inner); // Error: inner is not in scope
}
Block Scope
Variables declared in blocks have limited scope:
fn main() {
let condition = true;
let number = if condition {
let temp = 5;
temp * 2 // 10
} else {
let temp = 3;
temp * 2 // 6
};
println!("Number: {}", number);
// println!("{}", temp); // Error: temp is not in scope
}
Ownership and Variables
Variables in Rust have ownership over their data:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership moves from s1 to s2
// println!("{}", s1); // Error: s1 no longer owns the data
println!("{}", s2); // This works
}
Copy vs Move Semantics
Some types are copied instead of moved:
fn main() {
// Types that implement Copy are copied
let x = 5;
let y = x; // x is copied to y
println!("x: {}, y: {}", x, y); // Both are valid
// Types that don't implement Copy are moved
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // Error: s1 has been moved
}
References and Borrowing
You can use references to access data without taking ownership:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Borrow s1
println!("The length of '{}' is {}.", s1, len); // s1 is still valid
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but doesn't drop the data because it's a reference
Mutable References
You can create mutable references to change borrowed data:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Best Practices
1. Prefer Immutability
Start with immutable variables and only add mut
when necessary:
// Good: Clear intent
let name = "Alice";
let mut counter = 0;
// Avoid: Unnecessary mutability
// let mut name = "Alice"; // name never changes
2. Use Meaningful Names
Choose descriptive variable names:
// Good
let user_count = 42;
let is_authenticated = true;
let maximum_retries = 3;
// Avoid
let n = 42;
let flag = true;
let max = 3;
3. Minimize Scope
Declare variables as close to their use as possible:
fn process_data() {
// ... other code ...
if needs_processing {
let processed_data = expensive_computation(); // Declare when needed
use_data(processed_data);
}
}
4. Use Shadowing for Transformations
Leverage shadowing for data transformations:
fn parse_input(input: &str) -> Result<i32, std::num::ParseIntError> {
let input = input.trim(); // Remove whitespace
let input = input.parse::<i32>()?; // Parse to integer
Ok(input)
}
Common Patterns
Builder Pattern with Mutability
struct Config {
host: String,
port: u16,
timeout: u64,
}
impl Config {
fn new() -> Self {
Config {
host: "localhost".to_string(),
port: 8080,
timeout: 30,
}
}
fn host(mut self, host: String) -> Self {
self.host = host;
self
}
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
}
fn main() {
let config = Config::new()
.host("example.com".to_string())
.port(9000);
}
Accumulator Pattern
fn sum_even_squares(numbers: &[i32]) -> i32 {
let mut sum = 0;
for &num in numbers {
if num % 2 == 0 {
sum += num * num;
}
}
sum
}
Debugging Variables
Use debug printing to inspect variables:
fn main() {
let x = 5;
let y = 10;
println!("x = {}, y = {}", x, y);
println!("x = {:#?}", x); // Pretty debug print
// Using dbg! macro
let z = dbg!(x + y); // Prints and returns the value
}
Understanding variables and mutability is fundamental to writing effective Rust code. Rust's approach of immutability by default, combined with explicit mutability and ownership rules, helps create safer and more maintainable programs while maintaining excellent performance.