5.4 Variables and Mutability
Variables in programming represent a named space in memory where data can be stored and accessed. They allow you to store values, manipulate them, and retrieve them later in your program. In Rust, every variable has a well-defined data type, which is determined when the variable is declared and cannot change afterward.
5.4.1 Declaring Variables
In Rust, variables are declared using the let
keyword. By default, variables are immutable, meaning once a value is assigned, it cannot be changed. This immutability helps prevent unintended changes to data, improving the safety and reliability of the program.
Example:
#![allow(unused)] fn main() { let x = 5; println!("The value of x is: {}", x); }
In this example, x
is an immutable variable with the value 5
. The println!()
macro, similar to printf()
in C, is used to print values to the terminal window.
5.4.2 Type Annotations and Type Inference
In Rust, you can specify the data type of a variable explicitly using a type annotation, or you can let the compiler infer the type based on the value.
Example with type annotation:
#![allow(unused)] fn main() { let x: i32 = 10; // Explicitly specifying the type println!("The value of x is: {}", x); }
Example with type inference:
#![allow(unused)] fn main() { let y = 20; // The compiler infers that y is an i32 println!("The value of y is: {}", y); }
In the second example, since 20
is an integer literal, the compiler automatically infers that y
has the type i32
.
Rust's type inference is highly intelligent and often determines the most appropriate type based on how a variable is used. For example, when an integer variable is used as an array index, Rust may infer the usize
type instead of the default i32
.
5.4.3 Mutable Variables
If you need a variable whose value can change, you can declare it as mutable using the mut
keyword.
Example:
// this example is editable fn main() { let mut z = 30; println!("The initial value of z is: {}", z); z = 40; println!("The new value of z is: {}", z); }
In this example, z
is declared as mutable, allowing its value to be changed from 30
to 40
. While mutable variables are useful when values need to change, immutability by default encourages safer, more predictable code.
5.4.4 Why Immutability by Default?
Immutability is the default in Rust because it promotes safety and helps avoid bugs caused by unexpected data changes. Immutable data can also be shared across threads without the need for synchronization, making it safer and more efficient in concurrent programs.
5.4.5 Constants
Constants in Rust are similar to immutable variables, but they differ in important ways:
- Constants are declared using the
const
keyword. - Constants must have their type explicitly stated.
- Constants are evaluated at compile time and can be used across the entire program, unlike variables that are initialized at runtime.
- Constants can only be set to a constant expression, not the result of a function call or any other runtime computation.
Example:
const MAX_POINTS: u32 = 100_000; fn main() { println!("The maximum points are: {}", MAX_POINTS); }
Constants are typically used for values that should never change, like configuration parameters or limits. Unlike variables, constants are not part of the program’s runtime memory management, making them very efficient.
5.4.6 Shadowing and Re-declaration
In Rust, you can redeclare a variable with the same name using the let
keyword, even with a different type. This is called shadowing.
Example:
#![allow(unused)] fn main() { let spaces = " "; let spaces = spaces.len(); println!("The number of spaces is: {}", spaces); }
In this example, the variable spaces
is first declared as a string, and then it is shadowed to hold an integer representing the length of the string. Shadowing allows you to reuse variable names without mutability and with the flexibility to change types when needed.
5.4.7 Deferred Initialization
In Rust, a variable can be declared without an initial value, as long as it is assigned a value before being used. Rust ensures that all variables have well-defined values, preventing bugs caused by uninitialized memory.
Example:
#![allow(unused)] fn main() { let a; // Declare without initialization a = 42; // Assign a value later println!("The value of a is: {}", a); }
Deferred initialization can be useful when the assigned value depends on a condition, as shown below:
let a; // Immutable variable declared without initialization
if some_condition {
a = 42;
} else {
a = 7;
}
However, in simple cases like this, an if
expression could be used instead:
let a = if some_condition {
42
} else {
7
};
If you attempt to use a variable before it is initialized, Rust will not compile the code, ensuring that no variable is ever left uninitialized.
5.4.8 Scopes and Deallocation
In Rust, variables have a scope, which determines where they are valid and when they are dropped (freed). A variable’s scope begins when it is declared and ends when it goes out of scope, typically at the end of a block (e.g., a function or conditional block). Rust also deallocates variables when they are used for the last time, potentially freeing memory earlier than the end of the scope.
Example:
fn main() { let b = 5; { let c = 10; println!("Inside block: b = {}, c = {}", b, c); } // c is no longer accessible here println!("Outside block: b = {}", b); }
In this example, c
goes out of scope when the inner block ends and is deallocated, while b
remains accessible outside the block.
5.4.9 Global Variables and Constants
Rust generally avoids the use of global variables because they can lead to bugs and complexity in large programs. However, global constants are common practice in Rust and provide a safe way to share values across different parts of the program without risking data corruption.
Example of a global constant:
const PI: f64 = 3.1415926535; fn main() { println!("The value of PI is: {}", PI); }
5.4.10 Declaring Multiple Entities with let
or const
In Rust, each variable or constant must be declared with its own let
or const
statement. However, you can declare multiple variables in a single line by separating the declarations with semicolons or by destructuring a tuple.
Example with semicolons:
fn main() { let x = 5.0; let i = 10; println!("x = {}, i = {}", x, i); }
Example using tuple destructuring:
fn main() { let (x, i) = (5.0, 10); println!("x = {}, i = {}", x, i); }
This requirement promotes clarity and avoids ambiguity in complex declarations. For constants, each must also be declared individually, ensuring that their types are explicitly defined.