Rust Coding Questions and Answers

Welcome to this curated collection of Rust coding questions and answers. As your friendly librarian, I have gathered some of the most important concepts to help you master Rust!

You'll find questions separated into:

  • Basic Concepts
  • Advanced Concepts

Q: What is the difference between String and &str in Rust?

Answer:

In Rust, String and &str are both used to handle string data, but they have distinct differences in how they manage memory and handle ownership:

  1. String:

    • It is an owned type.
    • It is stored on the heap, meaning its size can grow or shrink at runtime.
    • When a String goes out of scope, its memory is automatically freed (dropped).
    • You can mutate it (if it's declared mut), e.g., by pushing new characters or strings to it.
  2. &str (String Slice):

    • It is a borrowed type (a reference).
    • It represents a view into a block of memory that contains a string (which could be on the heap, stack, or hardcoded in the binary as a static string).
    • It does not have ownership of the data it points to; it merely looks at it temporarily.
    • It is immutable by default and its size is fixed.

Example:

fn main() {
    // A String (heap-allocated, owned, growable)
    let mut my_string = String::from("Hello");
    my_string.push_str(", world!");

    // A &str (string slice, borrowed, fixed-size view)
    let my_slice: &str = &my_string[0..5]; // Borrows "Hello"
    
    // Hardcoded string literals are also of type &str (specifically &'static str)
    let static_str: &str = "I am stored in the binary";
}

Q: Explain Ownership and Borrowing in Rust.

Answer:

Ownership is Rust's most unique feature, which guarantees memory safety without needing a garbage collector. It operates on three main rules:

  1. Each value in Rust has a single variable that is its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped (its memory is freed).

Borrowing is how Rust allows you to access a value without taking ownership of it, using references (& or &mut). It solves the problem of needing to pass values to functions without losing ownership.

Borrowing has two strict rules:

  1. At any given time, you can have either one mutable reference (&mut T) or any number of immutable references (&T).
  2. References must always be valid (Rust prevents dangling pointers).

Example of Ownership:

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership moves to s2
// println!("{}", s1); // Error! s1 is no longer valid
}

Example of Borrowing:

#![allow(unused)]
fn main() {
fn calculate_length(s: &String) -> usize { // Takes an immutable reference
    s.len()
} // s goes out of scope, but since it doesn't have ownership, nothing is dropped.

let s1 = String::from("hello");
let len = calculate_length(&s1); // We borrow s1 instead of moving it
}

Q: How do you handle errors in Rust?

Answer:

Rust groups errors into two major categories: Recoverable and Unrecoverable errors.

1. Recoverable Errors (Result<T, E>)

For errors that can be handled gracefully (like a file not being found), Rust uses the Result enum:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

You can handle Result using match or the ? operator.

Using match:

#![allow(unused)]
fn main() {
use std::fs::File;

let f = File::open("hello.txt");
let f = match f {
    Ok(file) => file,
    Err(error) => panic!("Problem opening the file: {:?}", error),
};
}

Using the ? Operator: The ? operator is a shorthand that unwraps Ok values or immediately returns the Err from the current function.

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
}

2. Unrecoverable Errors (panic!)

For situations where the program reaches a state that it cannot recover from (like accessing an array out of bounds), Rust provides the panic! macro. When a panic occurs, the program will print a failure message, unwind and clean up the stack, and then quit.

fn main() {
    panic!("crash and burn");
}

Q: How does Pattern Matching work in Rust?

Answer:

Pattern matching in Rust is extremely powerful and allows you to compare a value against a series of patterns and execute code based on which pattern matches. This is primarily done using the match and if let constructs.

1. The match Operator

match takes a value and routes control to branches (arms) based on matching patterns. It is exhaustive, meaning every possible case must be covered.

#![allow(unused)]
fn main() {
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String), // Can hold data
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {}!", state);
            25
        }
    }
}
}

2. if let Syntax

When you only care about matching one specific pattern while ignoring the rest, match can be overly verbose. In these cases, you can use if let.

#![allow(unused)]
fn main() {
let some_u8_value = Some(0u8);

// Using match:
match some_u8_value {
    Some(3) => println!("three"),
    _ => (), // Does nothing for other values
}

// Using if let (more concise):
if let Some(3) = some_u8_value {
    println!("three");
}
}