Skip to Content
RustBooksThe Rust BookCh 3. Common programming concepts

Ch 3. Common programming concepts

Ch 3.1 Variables and Immutability

  • Default: Variables in Rust are immutable by default.

  • Immutability Example (does not compile):

    fn main() { let x = 5; println!("The value of x is: {x}"); x = 6; // Error: cannot assign twice to an immutable variable println!("The value of x is: {x}"); }

Mutability

  • Why Immutability Matters:

    • Immutability helps avoid bugs caused by unexpected changes to values, making programs safer and easier to reason through.
    • Rust enforces immutability at compile time, preventing runtime errors from value changes.
  • Making Variables Mutable: Use mut to allow changes to a variable’s value.

    fn main() { let mut x = 5; println!("The value of x is: {x}"); x = 6; // Now allowed with `mut` println!("The value of x is: {x}"); }
  • Use Case: Mutability is useful when a variable’s value needs to change throughout the program, and using mut conveys this intent to others reading the code.

Constants

  • Naming Convention: Constants use uppercase with underscores.

  • Declaration: Use const for constants, which are always immutable and require type annotations.

    const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
  • Key Differences:

    • Constants cannot be made mutable and must be assigned a value that can be evaluated at compile time.
    • They can be declared in any scope (including global) and are valid for the entire program duration.

Shadowing

  • Shadowing Variables: Reuse a variable name by redeclaring it with let, allowing transformations or changes in type.

    fn main() { let x = 5; let x = x + 1; // Shadowing the first `x` { let x = x * 2; // Shadowing within an inner scope println!("The value of x in the inner scope is: {x}"); // 12 } println!("The value of x is: {x}"); // 6 }
  • Differences from mut:

    • Shadowing allows changing both the value and type of a variable, whereas mut only allows changing the value without changing the type.
    • Example of type change with shadowing:
    let spaces = " "; let spaces = spaces.len(); // spaces is now an integer (usize)
    • Shadowing prevents accidental reassignment errors, enforcing immutability after transformations.
  • Example Error when using mut to change types:

    let mut spaces = " "; spaces = spaces.len(); // Error: expected `&str`, found `usize`

3.2 Data Types

Rust is a statically typed language, meaning all types must be known at compile time. Rust often infers types, but sometimes you need to specify them explicitly, like when parsing strings into numbers:

let guess: u32 = "42".parse().expect("Not a number!");

Scalar Types

A scalar type represents a single value. Rust has four primary scalar types:

  1. Integer Types:

    • Signed (i) and Unsigned (u) types of various sizes (8-bit to 128-bit, and isize/usize for architecture-dependent sizes).
    • Default integer type: i32.
    • Integer Overflow: In debug mode, Rust panics on overflow. In release mode, Rust uses two’s complement wrapping.
    • Integer literals can be written in decimal, hexadecimal, octal, binary, and byte notation.

    Example:

    let x: u8 = 255; // Unsigned 8-bit integer
  2. Floating-Point Types:

    • Types: f32 (single precision) and f64 (double precision, default).
    • Example:
    let x = 2.0; // f64 let y: f32 = 3.0; // f32
  3. Boolean Type (bool):

    • Two possible values: true and false.
    • Example:
    let t = true; let f: bool = false;
  4. Character Type (char):

    • Represents a Unicode scalar value (4 bytes).
    • Can store more than just ASCII characters, including emojis.
    • Example:
    let c = 'z'; let z: char = 'ℤ'; let heart = '❤️';

Compound Types

Compound types can group multiple values into one type. Rust has two primitive compound types:

  1. Tuples:

    • Group multiple values with different types into one compound type.
    • Tuples have a fixed length and can be destructured.

    Example:

    let tup: (i32, f64, u8) = (500, 6.4, 1); let (x, y, z) = tup; // Destructuring println!("y: {y}"); // Accessing elements
    • Access individual elements by index:
    let x = tup.0; let y = tup.1;
  2. Arrays:

    • Fixed-size collection of values of the same type.
    • Arrays are stored on the stack, unlike vectors, which can change in size and are stored on the heap.

    Example:

    let a = [1, 2, 3, 4, 5]; let months = ["Jan", "Feb", "Mar"];
    • Array initialization with the same value:
    let a = [3; 5]; // Equivalent to [3, 3, 3, 3, 3]
    • Access elements via index:
    let first = a[0];

Handling Invalid Array Indexes

  • If you access an invalid array index, Rust will panic at runtime, protecting against unsafe memory access.

Example of invalid index causing a panic:

let a = [1, 2, 3, 4, 5]; let element = a[10]; // Panic: index out of bounds

3.3 Functions

  • Function Declaration: Use the fn keyword, followed by the function name (snake case), and parentheses ().

    fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); }
  • Calling Functions: Call a function by its name followed by parentheses. The order of function definition does not matter, as long as the function is in scope.

Parameters

  • Functions can take parameters (input values) defined in the function signature. You must declare the type of each parameter.

    fn another_function(x: i32) { println!("The value of x is: {x}"); } fn main() { another_function(5); // Passing 5 as an argument }
    • Multiple Parameters: Separate parameters with commas.

      fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }

Statements and Expressions

  • Statements: Perform actions but do not return values (e.g., variable declarations).

    fn main() { let y = 6; // Statement }
    • You cannot assign a statement to a variable because it doesn’t return a value.

      let x = (let y = 6); // Error: expected expression, found statement
  • Expressions: Evaluate to a value. Most operations in Rust (e.g., arithmetic, function calls, blocks) are expressions.

    fn main() { let y = { let x = 3; x + 1 // Expression: returns 4 }; println!("The value of y is: {y}"); // Prints "The value of y is: 4" }
    • Semicolon in Expressions: Adding a semicolon turns an expression into a statement, which will not return a value.

Function Return Values

  • Return Values: Functions can return values using an arrow (->) to specify the return type.

    fn five() -> i32 { 5 // Return value (no semicolon) } fn main() { let x = five(); println!("The value of x is: {x}"); // Prints "The value of x is: 5" }
  • Implicit Return: The last expression in a function is implicitly returned if it does not end with a semicolon. For example:

    fn plus_one(x: i32) -> i32 { x + 1 // No semicolon, so this value is returned }
  • Explicit Return: You can also use the return keyword to return early.

    fn early_return(x: i32) -> i32 { if x > 5 { return x; // Return early } x + 1 // Otherwise, return x + 1 }

Common Errors

  • Mismatched Return Types: A semicolon at the end of an expression turns it into a statement, leading to errors when a value is expected:

    fn plus_one(x: i32) -> i32 { x + 1; // Error: this is now a statement, so it returns `()`, not `i32` }
    • Fix by removing the semicolon:
    fn plus_one(x: i32) -> i32 { x + 1 // Now returns the value }

3.4 Comments

Single-Line Comments

  • Use two slashes (//) to write comments. Everything after // is ignored by the compiler.

    // This is a comment

Multi-Line Comments

  • For longer comments, use // on each line:

    // This is a longer comment that // spans multiple lines.

Inline Comments

  • You can also place comments at the end of code lines:

    let lucky_number = 7; // I’m feeling lucky today

Idiomatic Comment Placement

  • Typically, comments are placed above the code they annotate for better readability:

    // I’m feeling lucky today let lucky_number = 7;

3.5 Control flow

if Expressions

let number = 6; if number % 4 == 0 { println!("divisible by 4"); } else if number % 3 == 0 { println!("divisible by 3"); } else if number % 2 == 0 { println!("divisible by 2"); } else { println!("not divisible by 4, 3, or 2"); }
  • Using if in let Statements: You can assign the result of an if expression to a variable.

    let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}");
  • Type Consistency in if: The result of both the if and else arms must have the same type.

    let number = if condition { 5 } else { "six" }; // Error: mismatched types

loop (Infinite Loop)

  • Repeats a block of code forever unless explicitly stopped with break.

    loop { println!("again!"); }
  • Breaking from a loop: Use break to exit a loop.

    let result = loop { counter += 1; if counter == 10 { break counter * 2; // return value } }; // result is 20, e.g. the returned value of counter * 2 println!("The result is {result}");
    • You might also need to pass the result of that operation out of the loop to the rest of your code. To do this, you can add the value you want returned after the break expression you use to stop the loop; that value will be returned out of the loop so you can use i
  • Loop Labels: Used to specify which loop to break from in nested loops.

    'outer: loop { loop { break 'outer; // Breaks the outer loop } }

while Loops

  • Executes code while a condition is true.

    let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!");

for Loops

  • Used to iterate over a collection or a range.

    let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); }
  • Iterating over a Range: You can use ranges to loop through numbers.

    for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!");
Last updated on