Introduction to Rust Variables
Variables are fundamental building blocks in any programming language, used to store and manipulate data. Rust's approach to variables is unique, emphasizing safety, immutability by default, and clear ownership semantics. Understanding variables in Rust is crucial for writing efficient and safe code.
Key Concepts
- Immutability by Default: Variables are immutable unless explicitly made mutable
- Type Safety: Every variable has a compile-time known type
- Ownership: Each value has a single owner variable
- Shadowing: Reusing variable names with new bindings
- Memory Safety: Automatic memory management without garbage collection
1. Variable Declaration
Basic Syntax
fn main() {
// Immutable variable (default)
let x = 5;
// x = 6; // This would cause a compile error!
// Mutable variable
let mut y = 5;
y = 6; // This is allowed
println!("y = {}", y);
// Type annotation
let z: i32 = 10;
let name: &str = "Alice";
let is_active: bool = true;
// Multiple declarations
let a = 5;
let b = 10;
// Declare without initializing (rare, but possible)
let uninitialized_var;
// Cannot use uninitialized_var here
uninitialized_var = 42; // Now it's initialized
println!("uninitialized_var = {}", uninitialized_var);
}
Naming Conventions
fn main() {
// Valid variable names
let name = "Alice";
let first_name = "Bob";
let _unused = 42; // Underscore prefix suppresses unused warning
let r#type = "special"; // Raw identifier (rare, for keywords)
// Case conventions
let snake_case = "variables"; // Rust uses snake_case for variables
let SCREAMING_SNAKE_CASE = "constants"; // For constants and statics
let camelCase = "not_typical"; // Not idiomatic in Rust
// Numbers in names
let version2 = "2.0";
let item_1 = "first";
// Unicode is allowed (but not recommended)
let 名字 = "name in Chinese"; // Valid but avoid for portability
}
2. Immutability vs Mutability
Immutable Variables (Default)
fn main() {
// Immutable by default - cannot change
let x = 5;
println!("x = {}", x);
// This would fail:
// x = 10; // ERROR: cannot assign twice to immutable variable
// Benefits of immutability:
// - Thread safety
// - Easier to reason about code
// - Prevents accidental modifications
// Immutable reference
let s1 = String::from("hello");
let s2 = &s1; // Borrow immutably
println!("s1 = {}, s2 = {}", s1, s2); // Both can be read
// Immutable struct
struct Point {
x: i32,
y: i32,
}
let point = Point { x: 10, y: 20 };
// point.x = 30; // ERROR: cannot mutate immutable struct field
}
Mutable Variables
fn main() {
// Mutable variable - can change
let mut x = 5;
println!("Initial x = {}", x);
x = 10;
println!("Modified x = {}", x);
x += 5;
println!("After addition x = {}", x);
// Mutable string
let mut s = String::from("hello");
s.push_str(", world");
println!("s = {}", s);
// Mutable struct
struct Point {
x: i32,
y: i32,
}
let mut point = Point { x: 10, y: 20 };
point.x = 30; // Allowed because point is mutable
println!("point.x = {}", point.x);
// Mutable array
let mut arr = [1, 2, 3, 4, 5];
arr[0] = 10;
println!("arr = {:?}", arr);
// Mutable vector
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(3);
println!("vec = {:?}", vec);
}
Performance Considerations
fn main() {
// Immutable variables can enable compiler optimizations
let x = 5; // Compiler knows this never changes
// Mutable variables have a small overhead but enable flexibility
let mut counter = 0;
for i in 0..10 {
counter += i; // Need mutability here
}
// Large immutable data structures can be shared safely
let large_data = vec![0; 1_000_000];
// Cloning vs mutating
let mut data = vec![1, 2, 3];
// Mutation (efficient)
data.push(4);
// Immutable approach (creates new allocation)
let data2 = [&data[..], &[4]].concat();
}
3. Type Annotations and Inference
Type Inference
fn main() {
// Rust infers types from usage
let x = 5; // i32 (default integer type)
let y = 3.14; // f64 (default float type)
let z = "hello"; // &str
let v = vec![1, 2, 3]; // Vec<i32>
// Type inference works with generics
let mut numbers = Vec::new();
numbers.push(1); // Now numbers is Vec<i32>
// Complex inference
let result = (1..10)
.filter(|&x| x % 2 == 0)
.map(|x| x * x)
.collect::<Vec<_>>(); // Type specified at the end
println!("result = {:?}", result);
}
Explicit Type Annotations
fn main() {
// Basic type annotations
let x: i32 = 5;
let y: f64 = 3.14;
let z: bool = true;
let c: char = 'a';
let s: String = String::from("hello");
// Integer types
let a: i8 = 127; // -128 to 127
let b: u8 = 255; // 0 to 255
let c: i16 = 32767; // -32768 to 32767
let d: i32 = 2147483647; // -2^31 to 2^31-1
let e: i64 = 9223372036854775807;
let f: isize = 100; // Architecture-dependent
// Floating point
let g: f32 = 3.14;
let h: f64 = 2.71828;
// Complex types
let tuple: (i32, f64, &str) = (10, 3.14, "hello");
let array: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &array[1..3];
// Reference types
let ref_i: &i32 = &x;
let ref_mut: &mut i32 = &mut 5;
// Function pointers
let func: fn(i32) -> i32 = |x| x * 2;
}
Type Casting and Conversion
fn main() {
// Using 'as' for primitive conversion
let x: i32 = 5;
let y: f64 = x as f64;
let z: i16 = x as i16;
// Potential loss of data
let big_num: i32 = 1000;
let small_num: i8 = big_num as i8; // Wraps around (becomes -24)
println!("1000 as i8 = {}", small_num); // -24
// Safe conversions with From/Into
let num = 5;
let num_str: String = num.to_string();
// Parsing strings
let parsed: i32 = "42".parse().unwrap();
let parsed2 = "42".parse::<i32>().unwrap();
// Using try_from for safe conversions
use std::convert::TryFrom;
let large = 256;
match i8::try_from(large) {
Ok(val) => println!("Converted: {}", val),
Err(e) => println!("Failed: {}", e),
}
}
4. Variable Scope and Lifetime
Block Scope
fn main() {
// Variables are scoped to blocks
let x = 5;
{
// Inner scope
let y = 10;
println!("Inner x = {}, y = {}", x, y); // Can access outer x
// y is valid here
} // y goes out of scope
// println!("y = {}", y); // ERROR: y not found
// Shadowing in nested scopes
let z = 15;
{
let z = 20; // Shadows outer z
println!("Inner z = {}", z); // 20
}
println!("Outer z = {}", z); // 15
}
Function Scope
fn main() {
let outer_var = 10;
// Function call passes ownership or borrows
process_value(outer_var); // outer_var moved or copied
// println!("outer_var = {}", outer_var); // If moved, this fails
let reference = &outer_var;
process_ref(reference); // Borrowing
println!("Still can use: {}", outer_var);
}
fn process_value(x: i32) {
println!("Processing: {}", x);
} // x dropped here
fn process_ref(x: &i32) {
println!("Processing reference: {}", x);
} // Only reference dropped, original value remains
Lifetime Annotations
// Lifetime annotations specify how long references live
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(&string1, &string2);
println!("Longest: {}", result); // Valid here
} // string2 dropped
// println!("Longest: {}", result); // ERROR: string2 dropped
}
5. Shadowing
Basic Shadowing
fn main() {
// Shadowing with same type
let x = 5;
let x = x + 1; // Shadows previous x
let x = x * 2;
println!("x = {}", x); // 12
// Shadowing with different type
let name = "Alice";
let name = name.len(); // Now name is usize
println!("name length = {}", name);
// Shadowing in loops
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
// Shadowing with mutability change
let value = 5;
let mut value = value; // Now mutable
value += 5;
println!("value = {}", value);
}
Advanced Shadowing Patterns
fn main() {
// Parsing with shadowing
let input = "42";
let input: i32 = input.parse().unwrap();
let input = input * 2;
println!("input = {}", input);
// Option handling with shadowing
let maybe_value: Option<i32> = Some(10);
if let Some(value) = maybe_value {
// 'value' shadows the outer scope's variable
println!("Got value: {}", value);
}
// Shadowing in match expressions
let x = Some(5);
match x {
Some(x) => println!("Matched: {}", x), // x shadows outer x
None => (),
}
// Shadowing for transformations
let data = vec![1, 2, 3, 4, 5];
let data = data.iter().map(|&x| x * 2).collect::<Vec<_>>();
let data = data.iter().filter(|&&x| x > 5).collect::<Vec<_>>();
println!("Transformed data: {:?}", data);
}
6. Constants and Statics
Constants
// Constants are always immutable, evaluated at compile time
const MAX_POINTS: u32 = 100_000;
const GRAVITY: f64 = 9.81;
const APPLICATION_NAME: &str = "My Rust App";
// Constants can be computed at compile time
const SECONDS_IN_HOUR: u32 = 60 * 60;
const ARRAY_SIZE: usize = 5 * 4 * 3;
// Constants in different scopes
fn example() {
const LOCAL_CONST: i32 = 42;
println!("Local constant: {}", LOCAL_CONST);
}
fn main() {
println!("MAX_POINTS = {}", MAX_POINTS);
println!("GRAVITY = {}", GRAVITY);
println!("SECONDS_IN_HOUR = {}", SECONDS_IN_HOUR);
example();
// Constants can be used in array sizes
let arr: [i32; ARRAY_SIZE] = [0; ARRAY_SIZE];
println!("Array length: {}", arr.len());
}
Static Variables
// Static variables have a fixed memory location
static APP_NAME: &str = "My Application";
static VERSION: &str = "1.0.0";
// Mutable static (unsafe)
static mut COUNTER: u32 = 0;
// Static with complex type
static DEFAULT_CONFIG: Config = Config {
timeout: 30,
retries: 3,
};
struct Config {
timeout: u32,
retries: u32,
}
fn main() {
// Reading static is safe
println!("App: {}, Version: {}", APP_NAME, VERSION);
// Modifying mutable static requires unsafe
unsafe {
COUNTER += 1;
println!("Counter: {}", COUNTER);
}
// Statics have 'static lifetime
let static_ref: &'static str = APP_NAME;
}
Constants vs Statics
// Key differences:
// - Constants are inlined, statics have fixed memory location
// - Constants can only be primitive types, statics can be more complex
// - Constants are always immutable, statics can be mutable (unsafe)
const CONST_VAL: i32 = 42; // Inlined at compile time
static STATIC_VAL: i32 = 42; // Single memory location
fn main() {
// Each usage of CONST_VAL might create a new copy
let a = CONST_VAL;
let b = CONST_VAL;
// STATIC_VAL has the same address everywhere
println!("Static address: {:p}", &STATIC_VAL);
// Constants work in pattern matching, statics don't
match a {
CONST_VAL => println!("Matched constant!"),
_ => println!("No match"),
}
}
7. Variable Binding Patterns
Destructuring
fn main() {
// Tuple destructuring
let tuple = (1, "hello", 3.14);
let (a, b, c) = tuple;
println!("a = {}, b = {}, c = {}", a, b, c);
// Ignoring values with _
let (x, _, z) = (10, 20, 30);
println!("x = {}, z = {}", x, z);
// Struct destructuring
struct Point {
x: i32,
y: i32,
z: i32,
}
let point = Point { x: 10, y: 20, z: 30 };
let Point { x, y, z } = point;
println!("x = {}, y = {}, z = {}", x, y, z);
// Destructuring with renaming
let Point { x: a, y: b, z: c } = point;
println!("a = {}, b = {}, c = {}", a, b, c);
// Array destructuring
let arr = [1, 2, 3, 4, 5];
let [first, second, ..] = arr;
println!("first = {}, second = {}", first, second);
// Nested destructuring
let nested = (1, (2, 3), 4);
let (a, (b, c), d) = nested;
println!("a = {}, b = {}, c = {}, d = {}", a, b, c, d);
}
Pattern Matching in Bindings
fn main() {
// Conditional binding with if let
let optional = Some(5);
if let Some(x) = optional {
println!("x = {}", x);
}
// while let for loops
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
// match bindings
let value = Some(10);
match value {
Some(x) if x < 5 => println!("Small: {}", x),
Some(x) => println!("Large: {}", x),
None => println!("Nothing"),
}
// Reference patterns
let x = 5;
let y = &x;
match y {
&val => println!("Got value: {}", val),
}
// Equivalent with deref
match *y {
val => println!("Got value: {}", val),
}
}
8. Ownership and Borrowing
Ownership Rules
fn main() {
// Rule 1: Each value has an owner
let s1 = String::from("hello"); // s1 owns the string
// Rule 2: Only one owner at a time
let s2 = s1; // Ownership moves to s2
// println!("{}", s1); // ERROR: s1 no longer valid
// Rule 3: When owner goes out of scope, value is dropped
{
let s3 = String::from("temporary");
} // s3 dropped here
// Copy types (implement Copy trait)
let x = 5; // i32 implements Copy
let y = x; // x is copied, not moved
println!("x = {}, y = {}", x, y); // Both valid
}
Borrowing
fn main() {
let s = String::from("hello");
// Immutable borrow
let len = calculate_length(&s);
println!("Length of '{}' is {}", s, len);
// Mutable borrow
let mut s = String::from("hello");
change(&mut s);
println!("Modified: {}", s);
// Multiple borrows
let s = String::from("hello");
let r1 = &s;
let r2 = &s; // Multiple immutable borrows are fine
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used
let r3 = &mut s; // Now we can mutably borrow
println!("{}", r3);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
fn change(s: &mut String) {
s.push_str(", world");
}
Borrowing Rules
fn main() {
// Rule: Can't have mutable and immutable borrows simultaneously
let mut s = String::from("hello");
let r1 = &s; // immutable borrow
let r2 = &s; // immutable borrow
// let r3 = &mut s; // ERROR: cannot borrow as mutable
println!("{}, {}", r1, r2);
// r1 and r2 go out of scope here
let r3 = &mut s; // Now this is fine
println!("{}", r3);
// Rule: Can't have two mutable borrows
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // ERROR: cannot borrow as mutable more than once
// println!("{}", r1); // This would cause error if uncommented
}
9. Variable Storage Locations
Stack vs Heap
fn main() {
// Stack allocated (fixed size, known at compile time)
let x = 5; // i32 goes on stack
let y = true; // bool on stack
let z = 3.14; // f64 on stack
let arr = [1, 2, 3]; // array on stack (if size known)
// Heap allocated (dynamic size)
let s = String::from("hello"); // String data on heap
let vec = vec![1, 2, 3, 4, 5]; // Vec data on heap
let boxed = Box::new(5); // Box puts value on heap
// Box for recursive types
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
println!("{:?}", list);
}
Memory Layout
use std::mem;
fn main() {
// Size of types
println!("Size of i32: {} bytes", mem::size_of::<i32>());
println!("Size of bool: {} bytes", mem::size_of::<bool>());
println!("Size of String: {} bytes", mem::size_of::<String>());
println!("Size of Vec<i32>: {} bytes", mem::size_of::<Vec<i32>>());
// Alignment
println!("Alignment of i32: {}", mem::align_of::<i32>());
// Custom struct layout
struct MyStruct {
a: u8, // 1 byte
b: u32, // 4 bytes (requires padding)
c: u16, // 2 bytes
}
// Actual layout: a(1) + padding(3) + b(4) + c(2) + padding(2) = 12 bytes
println!("Size of MyStruct: {} bytes", mem::size_of::<MyStruct>());
}
10. Advanced Variable Techniques
Lazy Initialization
use std::cell::RefCell;
use std::sync::Once;
// Lazy static with Once
static mut LAZY_VAR: Option<String> = None;
static INIT: Once = Once::new();
fn get_lazy_var() -> &'static String {
unsafe {
INIT.call_once(|| {
LAZY_VAR = Some(String::from("Initialized lazily"));
});
LAZY_VAR.as_ref().unwrap()
}
}
// Using lazy_static crate pattern
struct LazyStatic {
value: RefCell<Option<String>>,
}
impl LazyStatic {
fn new() -> Self {
LazyStatic {
value: RefCell::new(None),
}
}
fn get(&self) -> String {
let mut value = self.value.borrow_mut();
if value.is_none() {
*value = Some(String::from("Computed lazily"));
}
value.clone().unwrap()
}
}
fn main() {
println!("First access: {}", get_lazy_var());
println!("Second access: {}", get_lazy_var());
let lazy = LazyStatic::new();
println!("First: {}", lazy.get());
println!("Second: {}", lazy.get());
}
Thread-Local Variables
use std::cell::RefCell;
use std::thread;
thread_local! {
static THREAD_COUNTER: RefCell<u32> = RefCell::new(0);
}
fn main() {
// Each thread gets its own copy
THREAD_COUNTER.with(|counter| {
*counter.borrow_mut() += 1;
println!("Main thread counter: {}", *counter.borrow());
});
let handle = thread::spawn(|| {
THREAD_COUNTER.with(|counter| {
*counter.borrow_mut() += 10;
println!("Spawned thread counter: {}", *counter.borrow());
});
});
handle.join().unwrap();
THREAD_COUNTER.with(|counter| {
println!("Main thread still has: {}", *counter.borrow());
});
}
Atomic Variables
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
// Atomic variables for thread-safe sharing
let counter = Arc::new(AtomicUsize::new(0));
let running = Arc::new(AtomicBool::new(true));
let mut handles = vec![];
for _ in 0..10 {
let counter = counter.clone();
let running = running.clone();
handles.push(thread::spawn(move || {
while running.load(Ordering::Relaxed) {
counter.fetch_add(1, Ordering::SeqCst);
thread::sleep(std::time::Duration::from_millis(10));
}
}));
}
thread::sleep(std::time::Duration::from_millis(100));
running.store(false, Ordering::SeqCst);
for handle in handles {
handle.join().unwrap();
}
println!("Final counter: {}", counter.load(Ordering::SeqCst));
}
11. Pattern Matching with Variables
Match Guards and Bindings
fn main() {
let pair = (2, -2);
match pair {
(x, y) if x == y => println!("Equal"),
(x, y) if x + y == 0 => println!("Opposites"),
(x, _) if x % 2 == 0 => println!("First is even"),
_ => println!("No match"),
}
// @ bindings
let value = Some(5);
match value {
Some(x @ 1..=5) => println!("Small number: {}", x),
Some(x @ 6..=10) => println!("Medium number: {}", x),
Some(x) => println!("Large number: {}", x),
None => println!("None"),
}
// Binding with destructuring
struct Person {
name: String,
age: u32,
}
let person = Person {
name: String::from("Alice"),
age: 30,
};
match person {
Person { name: n @ _, age: 30 } => println!("Age 30 person"),
Person { name: ref n, age } => println!("Person: {}", n),
}
}
Ref Patterns
fn main() {
let x = 5;
// ref borrows a value
let ref r = x; // Equivalent to let r = &x;
println!("{}", r);
// ref mut for mutable borrow
let mut y = 5;
let ref mut r2 = y;
*r2 = 10;
println!("y = {}", y);
// In patterns
let maybe_value = Some(String::from("hello"));
match maybe_value {
Some(ref s) => println!("Borrowed: {}", s), // s is &String
None => (),
}
// Now we can still use maybe_value
println!("Still have: {:?}", maybe_value);
}
12. Performance Considerations
Copy vs Move Semantics
#[derive(Debug, Clone)]
struct LargeStruct {
data: [u8; 1024], // 1KB of data
}
fn main() {
// Copy types (cheap)
let x = 5;
let y = x; // Copy (no performance cost)
println!("x = {}, y = {}", x, y);
// Move types (ownership transfer)
let large = LargeStruct { data: [0; 1024] };
let large2 = large; // Move (no copy, just ownership transfer)
// println!("{:?}", large); // ERROR: large moved
// Clone (expensive, copies data)
let large = LargeStruct { data: [0; 1024] };
let large2 = large.clone(); // Copies 1KB of data
println!("Both exist: {:?} and {:?}", large.data[0], large2.data[0]);
// Best practice: borrow instead of move/clone when possible
let large = LargeStruct { data: [0; 1024] };
process_large(&large); // Borrow
println!("Still own large: {:?}", large.data[0]);
}
fn process_large(large: &LargeStruct) {
println!("Processing: {:?}", large.data[0]);
}
Variable Reuse and Optimization
fn main() {
// Reusing variable with shadowing (no new allocation necessarily)
let mut data = vec![1, 2, 3, 4, 5];
// Transform in place
for item in &mut data {
*item *= 2;
}
println!("Doubled: {:?}", data);
// Transform with new variable (may allocate)
let data_squared: Vec<_> = data.iter().map(|&x| x * x).collect();
println!("Squared: {:?}", data_squared);
// Reusing allocation with clear
let mut buffer = String::with_capacity(100);
for i in 0..10 {
buffer.clear(); // Reuse allocation
buffer.push_str(&format!("Line {}", i));
println!("{}", buffer);
}
}
Conclusion
Rust's variable system is designed around key principles:
Key Takeaways
- Immutability by Default: Variables are immutable unless explicitly marked
mut - Type Safety: Strong, static typing with excellent inference
- Ownership: Clear rules about who owns data
- Shadowing: Reuse variable names with new bindings
- Constants and Statics: Compile-time and runtime constants
- Pattern Matching: Destructuring and binding in patterns
- Memory Safety: No dangling references or data races
Best Practices
- Prefer immutable variables by default
- Use
mutonly when mutation is necessary - Leverage shadowing for type conversions
- Use
constfor compile-time constants - Use borrowing instead of ownership transfer when possible
- Be explicit with types in public APIs
- Use pattern matching for complex destructuring
Performance Guidelines
- Use copy types for small, frequently copied data
- Use references to avoid cloning large data
- Reuse allocations when possible
- Profile before optimizing variable usage
- Consider
Boxfor large types on the heap
Rust's variable system provides a solid foundation for writing safe, efficient, and maintainable code. Understanding these concepts is essential for mastering the language.