Variables and Boxes
Computers store values in memory. Sometimes we care about the values, but other times we care about the memory locations where the program stores values. For example, for a program that reads an integer from the input and writes the integer to the output, it doesn't know the input value at compile time, but it can ensure that the read and write use the same memory location. Rust uses variables to represent memory locations.1
Strictly speaking, variables represent memory on the stack, and boxes represent memory on the heap. We will discuss boxes later.
let
statement
The let
statement names a variable and binds it to a memory location on the stack. Additionally, this statement can annotate the type of the variable and initialize its value.
# #![allow(unused_variables)] #fn main() { // Binds a variable let x; x = 1; // Binds a variable and annotates its type let y: i32; // Binds a variable, annotates its type, and initializes its value. let z: i32 = 1; // Binds a variable and initialize its value, but lets the compiler infer its type. let w = 1; // The compiler infers that the type of `w` is `i32`. #}
A program may bind multiple variables in the same let
statement using tuples:
# #![allow(unused_variables)] #fn main() { let (x, y, z) = (1, 2.0, "Hello, world"); assert_eq!(x, 1); assert_eq!(y, 2.0); assert_eq!(z, "Hello, world"); #}
Assignment and mutability
If you didn't initialize a variable when binding it, you must assign it a value before reading it. The compiler prevents you from using uninitialized variables.
# #![allow(unused_variables)] #fn main() { let x; x = 1; assert_eq!(x, 1); let y: i32; // Error: use of uninitialized variable assert_eq!(y, 1); #}
In many other languages, a program may modify the value of any variable. However, modifying variables changes the state of the program, which may cause bugs. By contrast, reading variable values is safe because it does not change the state of the program. As a good software engineering practice, variables are immutable by default in Rust. Immutable variables also allows the compiler to optimize the program better. If a program wishes to modify a variable, it must use the mut
keyword in the let
statement:
# #![allow(unused_variables)] #fn main() { // `x` is immutable let x = 1; // `y` is immutable let y; // `z` is mutable let mut z = 1; // Error: `x` is immutable x = 1; // OK: initialize `y` y = 1; // Error: `y` is immutable y = 1; // OK: `z` is mutable z = 2; #}
In a let
statement binding multiple variables, mut
annotates only the variable following it:
// Only `y` is mutable. `x` and `z` are immutable
let (x, mut y, z);
Scope
When a program rebinds a variable, it frees the memory location of (and destroys) the current value and then allocates a new memory location for the variable.
# #![allow(unused_variables)] #fn main() { let x = 1; assert_eq!(x, 1); let x = "Hello, world"; assert_eq!(x, "Hello, world"); #}
If a program wishes to rebind a variable temporarily without destroying its current value, it can introduce a new scope.
-
A block is a region of the program enclosed by a pair of braces
{...}
. -
A scope of a variable is the block where the variable is bound.
A variable is visible only in its scope. However, if a variable is rebound in a nested scope, the variable in the parent scope becomes invisible in the nested scope (the variable in the nest scope shadows the variable in the parent scope), but it will become visible again when the program exits the nested scope to return to the parent scope.
# #![allow(unused_variables)] #fn main() { // Parent scope let x = 1; { // `x` in this nested scope shadows `x` in the parent scope. let x = "Hello, world"; assert_eq!(x, "Hello, world"); } assert_eq!(x, 1); #}
A variable goes out of scope when the program leaves the scope where the variable is bound. The program frees the variable's memory and destroys the variable's data.2
"Destroy" means that if the type of the data implements the Drop
trait, the program invokes Drop::drop()
on the data, which has a similar effect as C++'s destructor.
Boxes
To place a value on the heap, the program creates a box. This is similar to C's malloc
function or C++'s new
operator. In the following example, the program places the value 1
on the heap, and creates a variable x
on the stack to point to the value on the heap.
# #![allow(unused_variables)] #fn main() { let x = Box::new(1); #}
We say that the box variable x
owns the value 1
on the heap. When the box variable goes out of scope, the compiler automatically drops its value on the heap. No garbage collection or manual drop is necessary. By contrast, C and C++ require the program to free heap values manually, which is error prone, and Java relies on garbage collection to free heap values, which results in slower, unpredictable performance.