Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust FAQ: Basics of the Paradigm

PART03 -- Basics of the Paradigm

Q12: What is a struct?

A struct in Rust is a way to create your own custom data type by grouping related pieces of data together. Think of it like a blueprint for organizing information, such as a person’s name, age, and email. Structs let you bundle these fields into one unit, making your code cleaner and easier to work with.

For example, imagine you’re making a game and need to store info about a player. You could use a struct like this:

#![allow(unused)]
fn main() {
struct Player {
    name: String,
    score: i32,
    level: u32,
}
}

Here, Player is a struct with three fields: name (a string), score (a signed integer), and level (an unsigned integer). You can create a Player like this:

#![allow(unused)]
fn main() {
let player = Player {
    name: String::from("Alice"),
    score: 100,
    level: 1,
};
}

Structs are great because they let you organize data logically and access it using dot notation (e.g., player.name). Rust also has tuple structs (for unnamed fields) and unit structs (for no fields), but regular structs are the most common.

Q13: What is an enum?

An enum in Rust is a type that can represent one of several possible variants. It’s perfect for situations where something can be in one of a few distinct states, like a traffic light being red, yellow, or green. Each variant can optionally hold data, making enums super flexible.

For example, here’s an enum for a traffic light:

#![allow(unused)]
fn main() {
enum TrafficLight {
    Red,
    Yellow,
    Green,
}
}

You could also make an enum where variants hold data, like a message type:

#![allow(unused)]
fn main() {
enum Message {
    Text(String),
    Number(i32),
    Empty,
}
}

Here, Text holds a String, Number holds an i32, and Empty has no data. You can use enums with match to handle each variant:

#![allow(unused)]
fn main() {
let msg = Message::Text(String::from("Hello"));
match msg {
    Message::Text(text) => println!("Text message: {}", text),
    Message::Number(num) => println!("Number: {}", num),
    Message::Empty => println!("No message"),
}
}

Enums are awesome for modeling choices or states in a clear, safe way, and they’re a key part of Rust’s safety features.

Q14: What is a reference in Rust?

A reference in Rust is a way to “borrow” data without taking ownership of it. It’s like looking at a book in a library instead of taking it home—you can read it, but you don’t own it, and you can’t destroy it. References let you access data safely while following Rust’s strict rules to prevent bugs like dangling pointers or data races.

There are two types of references:

  • Immutable reference (&T): Lets you read data but not change it.
  • Mutable reference (&mut T): Lets you read and change data, but only one mutable reference can exist at a time.

For example:

#![allow(unused)]
fn main() {
let x = 10;
let r = &x; // Immutable reference to x
println!("Value through reference: {}", r);
}

Here, r is a reference to x, so you can read x’s value. Rust ensures references are safe by enforcing rules like “you can’t have a mutable reference if immutable references exist.”

Q15: What happens if you assign to a reference?

In Rust, assigning to a reference depends on whether it’s immutable (&T) or mutable (&mut T):

  • Immutable Reference (&T): You can’t assign to it because it’s read-only. Trying to will cause a compile-time error. For example:
    #![allow(unused)]
    fn main() {
    let x = 10;
    let r = &x;
    *r = 20; // Error: cannot assign to immutable reference
    }
  • Mutable Reference (&mut T): You can assign to it, and it changes the original data. You use the * operator to “dereference” and modify the value. For example:
    #![allow(unused)]
    fn main() {
    let mut x = 10;
    let r = &mut x;
    *r = 20; // Changes x to 20
    println!("x is now: {}", x); // Prints 20
    }

Rust’s rules ensure only one mutable reference exists at a time, preventing accidental data corruption. Assigning to a reference is safe as long as you follow these rules.

Q16: How can you rebind a reference to a different value?

In Rust, references themselves are immutable in the sense that you can’t make a reference point to a different value after it’s created. Once a reference like &x is set to point to x, you can’t make it point to y. This is part of Rust’s safety guarantees to avoid dangling pointers.

However, you can create a new reference in a new scope or reassign a variable that holds a reference. For example:

#![allow(unused)]
fn main() {
let mut x = 10;
let mut y = 20;
let r = &x; // r points to x
println!("r points to: {}", r); // Prints 10
{
    let r = &y; // New reference in a new scope, points to y
    println!("r now points to: {}", r); // Prints 20
}
}

Here, you’re not rebinding the original r but creating a new one in a different scope. If you want to “switch” what a reference points to, you’d typically reassign a variable to a new reference, but you need to ensure lifetimes and borrowing rules are followed. For mutable references, it’s similar, but you must ensure no other references conflict.

Q17: When should I use references, and when should I use owned types?

Choosing between references (&T or &mut T) and owned types (like String or Vec<T>) depends on what you’re doing:

  • Use References:
    • When you want to borrow data temporarily without taking ownership. This avoids copying data, which is faster and saves memory.
    • For read-only access (use &T), like passing a string to a function that just reads it.
    • For modifying data in place (use &mut T), like updating a struct’s field without moving it.
    • Example:
      #![allow(unused)]
      fn main() {
      fn print_name(name: &str) { // Borrow with &str
          println!("Name: {}", name);
      }
      let s = String::from("Alice");
      print_name(&s); // Pass a reference, s still usable
      }
  • Use Owned Types:
    • When you need to own the data, like storing it in a struct or moving it to another function.
    • When the data needs to live longer than the current scope or be modified independently.
    • Example:
      #![allow(unused)]
      fn main() {
      struct User {
          name: String, // Owns the String
      }
      let s = String::from("Alice");
      let user = User { name: s }; // s is moved, no longer usable
      }
  • Rule of Thumb: Use references for temporary access (reading or modifying) to avoid copying. Use owned types when you need full control or the data needs to “live” somewhere else. Rust’s compiler will guide you if you get it wrong!

Q18: What are functions? What are their advantages? How are they declared?

A function in Rust is a block of code that performs a specific task, like calculating a number or printing a message. Functions make your code reusable, organized, and easier to read because you can call them whenever you need that task done.

Advantages:

  • Reusability: Write a function once, use it many times.
  • Clarity: Functions give meaningful names to tasks, like calculate_total, making code easier to understand.
  • Modularity: Break your program into smaller, testable pieces.
  • Abstraction: Hide complex logic behind a simple function call.

How to Declare: Functions in Rust are declared with the fn keyword, followed by the name, parameters (in parentheses), an optional return type (after ->), and a body in curly braces. For example:

#![allow(unused)]
fn main() {
fn add_numbers(a: i32, b: i32) -> i32 {
    a + b // Returns the sum (no semicolon means return)
}
}

You call it like this:

#![allow(unused)]
fn main() {
let result = add_numbers(5, 3); // result is 8
}
  • Key Points:
    • Parameters need types (e.g., a: i32).
    • The return type (e.g., -> i32) is optional if the function returns nothing (()).
    • Use return for early returns, or omit it and use the last expression (like a + b above).
    • Functions can be public (pub fn) to be used outside the module or private otherwise.

Functions are a core building block in Rust, and you can also use closures (like anonymous functions) for more flexibility.