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.