Variables, data types, functions, and control flow, the building blocks of every Rust program.
In Rust, variables are immutable by default. This is a key safety feature that prevents accidental modifications and makes code easier to reason about. To make a variable mutable, you explicitly use the mut keyword.
fn main() {
let x = 5;
println!("x = {}", x);
// This will cause a compile error:
// x = 6;
}
When you need to change a variable's value, explicitly declare it as mutable:
fn main() {
let mut x = 5;
println!("x = {}", x);
x = 6;
println!("x = {}", x);
}
Constants are always immutable and must be annotated with their type. They can be declared at module scope:
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159;
fn main() {
println!("Maximum points: {}", MAX_POINTS);
println!("Pi: {}", PI);
}
Constants cannot be shadowed, and they're evaluated at compile-time. Variables can be shadowed (redeclared with the same name), which allows changing a value and potentially its type.
Shadowing allows you to reuse variable names and even change types:
fn main() {
let x = 5;
let x = x + 1; // x is now 6
let x = x * 2; // x is now 12
// You can even change the type:
let spaces = " ";
let spaces = spaces.len(); // spaces is now a number (3)
println!("x = {}, spaces = {}", x, spaces);
}
Rust is a statically typed language, meaning the compiler must know the type of every variable at compile-time. Rust can often infer types, but you can also annotate them explicitly.
Scalar types represent single values. Rust has four primary scalar types.
Signed and unsigned integers of various sizes:
fn main() {
// Signed integers (can be negative)
let a: i32 = -42; // 32-bit signed
let b: i64 = -1000; // 64-bit signed
let c: i8 = -5; // 8-bit signed (range -128 to 127)
// Unsigned integers (always positive)
let d: u32 = 42; // 32-bit unsigned
let e: u64 = 1000; // 64-bit unsigned
let f: u8 = 255; // 8-bit unsigned (range 0 to 255)
// Without annotation, integers default to i32
let g = 100;
println!("a={}, b={}, c={}, d={}, e={}, f={}, g={}", a, b, c, d, e, f, g);
}
IEEE 754 standard floating-point numbers:
fn main() {
let x: f64 = 3.14159; // 64-bit float (default)
let y: f32 = 2.71828; // 32-bit float
// Operations on floats
let sum = x + y as f64;
let product = x * y as f64;
println!("sum = {}, product = {}", sum, product);
}
Logical true/false values:
fn main() {
let is_active: bool = true;
let is_ready = false; // Type inferred as bool
// Booleans are used in conditionals
if is_active && !is_ready {
println!("Active but not ready");
}
}
Unicode scalar values:
fn main() {
let c: char = 'z';
let emoji: char = '🦀';
let greek: char = 'π';
println!("char: {}, emoji: {}, greek: {}", c, emoji, greek);
}
Compound types group multiple values into one type. The two primitive compound types are tuples and arrays.
Fixed-length collections of values with potentially different types:
fn main() {
// Tuple with mixed types
let tup: (i32, f64, u8) = (500, 6.4, 1);
// Access tuple elements with destructuring
let (x, y, z) = tup;
println!("x = {}, y = {}, z = {}", x, y, z);
// Access tuple elements with dot notation
println!("First element: {}", tup.0);
println!("Second element: {}", tup.1);
// Empty tuple (unit type)
let unit: () = ();
}
Fixed-length collections of values of the same type:
fn main() {
// Array with explicit type and length
let a: [i32; 5] = [1, 2, 3, 4, 5];
// Array with repeated value
let b = [3; 5]; // [3, 3, 3, 3, 3]
// Access array elements
println!("First element: {}", a[0]);
println!("Last element: {}", a[4]);
// Iterate over array
for element in a.iter() {
println!("Element: {}", element);
}
// Get array length
println!("Array length: {}", a.len());
}
Functions are fundamental building blocks in Rust. They're declared with the fn keyword and can take parameters and return values.
fn add(a: i32, b: i32) -> i32 {
a + b
fn main() {
let result = add(5, 3);
println!("5 + 3 = {}", result);
}
In Rust, the last expression in a function is automatically returned. Note there's no semicolon on the final expression:
fn multiply(a: i32, b: i32) -> i32 {
a * b // No semicolon - this is a return statement
fn add_one(x: i32) -> i32 {
x + 1 // This will be returned
fn main() {
let result1 = multiply(4, 5);
let result2 = add_one(10);
println!("4 * 5 = {}", result1);
println!("10 + 1 = {}", result2);
}
Adding a semicolon after the last expression turns it into a statement, which means the function returns the unit type () instead of the expected value.
fn calculate_rectangle_area(width: f64, height: f64) -> f64 {
width * height
fn print_coordinates(x: i32, y: i32) {
println!("Coordinates: ({}, {})", x, y);
fn main() {
let area = calculate_rectangle_area(15.5, 8.0);
println!("Area: {}", area);
print_coordinates(10, 20);
}
Rust provides several ways to control the flow of your program: conditionals (if/else) and loops (loop, while, for).
fn check_number(n: i32) {
if n > 0 {
println!("{} is positive", n);
} else if n < 0 {
println!("{} is negative", n);
} else {
println!("{} is zero", n);
}
fn main() {
check_number(5);
check_number(-3);
check_number(0);
}
Using if as an Expression: Since if is an expression in Rust, you can use it to assign values:
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value is: {}", number); // Prints: The value is: 5
// Both branches must return the same type
let age = 25;
let category = if age >= 18 { "adult" } else { "minor" };
println!("Category: {}", category);
}
The loop keyword creates an infinite loop that runs until you explicitly break out:
fn main() {
let mut counter = 0;
loop {
println!("Counter: {}", counter);
counter += 1;
if counter == 3 {
break; // Exit the loop
}
}
println!("Loop finished");
}
Returning Values from Loops: You can return a value from a loop by using break with an expression:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 5 {
break counter * 2; // Break with a value
}
};
println!("Result: {}", result); // Prints: Result: 10
}
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!");
}
The for loop is the most idiomatic way to iterate in Rust:
fn main() {
// Iterate over a range
for i in 1..4 {
println!("{}!", i);
}
// Iterate with inclusive range (includes 5)
for i in 1..=5 {
println!("Number: {}", i);
}
// Iterate over array
let arr = [10, 20, 30, 40];
for element in arr.iter() {
println!("Element: {}", element);
}
// Reverse iteration
for i in (1..=3).rev() {
println!("{}!", i);
}
}
Comments are notes in your code that the compiler ignores. Rust supports single-line and documentation comments:
// This is a single-line comment
/* This is a multi-line comment
that can span multiple lines */
/// This is a documentation comment
/// It documents the next item (function, struct, etc.)
/// and appears in generated documentation.
fn documented_function() {
// Implementation here
//! This is a crate-level or module-level documentation comment
//! It documents the item that contains it.
Use /// before functions and structs to generate HTML documentation with cargo doc --open. This is a cornerstone of Rust's excellent documentation ecosystem.