Rust – Top 25 interview questions

Here are the top 25 interview questions for Rust, along with detailed answers and relevant examples.

1. What are the key features of Rust?

Answer:
Rust is a systems programming language known for its safety and performance. Key features include:

  • Memory Safety: Rust ensures memory safety without a garbage collector through ownership, borrowing, and lifetimes.
  • Concurrency: Rust’s ownership model prevents data races, making concurrent programming easier and safer.
  • Performance: Rust’s performance is comparable to C/C++ due to zero-cost abstractions and control over low-level details.
  • Expressiveness: Rust has powerful type systems and pattern matching, making code more expressive and easier to maintain.
  • Cargo: Rust’s package manager and build system simplifies managing dependencies and building projects.

2. Explain Rust’s ownership system.

Answer:
Rust’s ownership system governs memory management through three main rules:

  • Each value has a single owner.
  • When the owner goes out of scope, the value is dropped.
  • Values can be borrowed (immutably or mutably), but borrowing rules must be followed to prevent data races and dangling references.

Example:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 is moved to s2, s1 is no longer valid
    // println!("{}", s1); // This would cause a compile-time error
    println!("{}", s2); // Output: hello
}

3. What are the differences between String and &str in Rust?

Answer:

  • String: A heap-allocated, mutable sequence of characters.
  let mut s = String::from("hello");
  s.push_str(", world!");
  println!("{}", s); // Output: hello, world!
  • &str: A borrowed string slice, which can be part of a String or a string literal, and is immutable.
  let s: &str = "hello";
  println!("{}", s); // Output: hello

4. What are lifetimes in Rust?

Answer:
Lifetimes are a way of ensuring that references are valid for as long as they are used. They prevent dangling references and help the compiler understand how long references should be valid.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = "short";
    let result = longest(s1.as_str(), s2);
    println!("The longest string is {}", result);
}

5. Explain Rust’s concurrency model.

Answer:
Rust’s concurrency model emphasizes safety and freedom from data races. Key features include:

  • Ownership and borrowing: Ensures that data is accessed safely across threads.
  • Send and Sync traits: Types must implement these traits to be transferred or accessed across threads.
  • Fearless concurrency: Rust’s guarantees make writing concurrent code easier and safer.

Example using threads:

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
    }

    handle.join().unwrap();
}

6. How do you handle errors in Rust?

Answer:
Rust handles errors using the Result and Option enums.

  • Result<T, E>: Used for functions that can return an error.
  fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
      if denominator == 0.0 {
          Err(String::from("Cannot divide by zero"))
      } else {
          Ok(numerator / denominator)
      }
  }

  fn main() {
      match divide(4.0, 2.0) {
          Ok(result) => println!("Result: {}", result),
          Err(e) => println!("Error: {}", e),
      }
  }
  • Option<T>: Used for values that can be Some or None.
  fn find_index(arr: &[i32], target: i32) -> Option<usize> {
      for (index, &value) in arr.iter().enumerate() {
          if value == target {
              return Some(index);
          }
      }
      None
  }

  fn main() {
      let arr = [1, 2, 3, 4, 5];
      match find_index(&arr, 3) {
          Some(index) => println!("Found at index: {}", index),
          None => println!("Not found"),
      }
  }

7. What is pattern matching in Rust?

Answer:
Pattern matching in Rust is a powerful feature that allows you to match and destructure data using the match statement and other constructs like if let and while let.

fn main() {
    let number = 7;

    match number {
        1 => println!("One"),
        2 | 3 | 5 | 7 | 11 => println!("This is a prime number"),
        13..=19 => println!("A teen"),
        _ => println!("Other"),
    }
}

8. What is the difference between Rc and Arc in Rust?

Answer:

  • Rc<T> (Reference Counted): Used for single-threaded reference counting.
  use std::rc::Rc;

  let rc_a = Rc::new(5);
  let rc_b = Rc::clone(&rc_a);
  println!("{}", Rc::strong_count(&rc_a)); // Output: 2
  • Arc<T> (Atomic Reference Counted): Used for thread-safe reference counting.
  use std::sync::Arc;
  use std::thread;

  let arc_a = Arc::new(5);
  let arc_b = Arc::clone(&arc_a);

  let handle = thread::spawn(move || {
      println!("{}", arc_b);
  });

  handle.join().unwrap();

9. What are traits in Rust?

Answer:
Traits in Rust are a way to define shared behavior across different types. They are similar to interfaces in other languages.

trait Summary {
    fn summarize(&self) -> String;
}

struct Post {
    title: String,
    content: String,
}

impl Summary for Post {
    fn summarize(&self) -> String {
        format!("{}: {}", self.title, self.content)
    }
}

fn main() {
    let post = Post {
        title: String::from("Rust"),
        content: String::from("A systems programming language"),
    };
    println!("{}", post.summarize()); // Output: Rust: A systems programming language
}

10. Explain Rust’s module system.

Answer:
Rust’s module system helps organize code and manage its scope and privacy. Modules are defined using the mod keyword.

mod sound {
    pub mod instrument {
        pub fn clarinet() {
            println!("Playing the clarinet");
        }
    }
}

fn main() {
    sound::instrument::clarinet();
}

11. What is the ? operator in Rust?

Answer:
The ? operator is a shorthand for propagating errors. It returns the error if the Result is an Err, otherwise it returns the Ok value.

fn read_file(filename: &str) -> Result<String, std::io::Error> {
    let content = std::fs::read_to_string(filename)?;
    Ok(content)
}

fn main() {
    match read_file("example.txt") {
        Ok(content) => println!("{}", content),
        Err(e) => println!("Error: {}", e),
    }
}

12. What are macros in Rust and how do you define them?

Answer:
Macros in Rust are a way of writing code that writes other code, allowing for meta-programming. They are defined using the macro_rules! macro.

macro_rules! say_hello {
    () => {
        println!("Hello, world!");
    };
}

fn main() {
    say_hello!(); // Output: Hello, world!
}

13. Explain Rust’s borrow checker.

Answer:
Rust’s borrow checker enforces the rules of borrowing at compile time to ensure memory safety. It ensures that:

  • You cannot have mutable and immutable references to the same data at the same time.
  • References must always be valid.

Example:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // immutable borrow
    let r2 = &s; // immutable borrow
    // let r3 = &mut s; // would cause compile-time error

    println!("{} and {}", r1, r2); // r1 and r2 are no longer used
    let r3 = &mut s; // mutable borrow
    r3.push_str(", world");
    println!("{}", r3);
}

14. What is the difference between Box<T> and Rc<T>?

    Answer:

    • Box<T>: Provides heap allocation for a single owner. It is used when you want to transfer ownership of a value to the heap and ensure a single owner.
      let boxed_value = Box::new(5);
      println!("{}", boxed_value); // Output: 5
    • Rc<T>: Provides reference counting for multiple owners in single-threaded scenarios. It allows multiple parts of a program to share ownership of a value.
      use std::rc::Rc;
    
      let rc_value = Rc::new(5);
      let rc_value_clone = Rc::clone(&rc_value);
      println!("{}", Rc::strong_count(&rc_value)); // Output: 2

    15. What is the difference between Vec<T> and VecDeque<T> in Rust?

    Answer:

    • Vec<T>: A dynamic array that provides efficient access and modification at the end of the array. Operations like push and pop at the end are O(1), but inserting or removing elements in the middle is O(n).
      let mut vec = Vec::new();
      vec.push(1);
      vec.push(2);
      println!("{:?}", vec); // Output: [1, 2]
    • VecDeque<T>: A double-ended queue that allows efficient addition and removal of elements from both ends. Operations like push_front, push_back, pop_front, and pop_back are O(1).
      use std::collections::VecDeque;
    
      let mut deque = VecDeque::new();
      deque.push_back(1);
      deque.push_front(2);
      println!("{:?}", deque); // Output: [2, 1]

    16. How do you implement traits for a struct in Rust?

    Answer:
    You implement traits for a struct by using the impl keyword followed by the trait name.

    trait Summary {
        fn summarize(&self) -> String;
    }
    
    struct Post {
        title: String,
        content: String,
    }
    
    impl Summary for Post {
        fn summarize(&self) -> String {
            format!("{}: {}", self.title, self.content)
        }
    }
    
    fn main() {
        let post = Post {
            title: String::from("Rust"),
            content: String::from("A systems programming language"),
        };
        println!("{}", post.summarize()); // Output: Rust: A systems programming language
    }

    17. What are closures in Rust and how do you use them?

    Answer:
    Closures in Rust are anonymous functions that can capture variables from their enclosing scope. They are defined using || syntax.

    fn main() {
        let x = 10;
        let closure = |y| x + y;
        println!("{}", closure(5)); // Output: 15
    }

    18. Explain the use of the Option type in Rust.

    Answer:
    The Option type is used to represent values that can be either Some (with a value) or None. It is commonly used to handle the absence of a value without using null.

    fn find_index(arr: &[i32], target: i32) -> Option<usize> {
        for (index, &value) in arr.iter().enumerate() {
            if value == target {
                return Some(index);
            }
        }
        None
    }
    
    fn main() {
        let arr = [1, 2, 3, 4, 5];
        match find_index(&arr, 3) {
            Some(index) => println!("Found at index: {}", index),
            None => println!("Not found"),
        }
    }

    19. What is the difference between panic! and Result in Rust?

    Answer:

    • panic!: Causes the program to terminate immediately and unwind the stack. It is used for unrecoverable errors.
      fn main() {
          panic!("This is a panic");
      }
    • Result<T, E>: Represents recoverable errors. Functions return Result to indicate success (Ok) or failure (Err).
      fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
          if denominator == 0.0 {
              Err(String::from("Cannot divide by zero"))
          } else {
              Ok(numerator / denominator)
          }
      }
    
      fn main() {
          match divide(4.0, 2.0) {
              Ok(result) => println!("Result: {}", result),
              Err(e) => println!("Error: {}", e),
          }
      }

    20. How do you implement a custom iterator in Rust?

    Answer:
    To implement a custom iterator, you need to define a struct and implement the Iterator trait for it.

    struct Counter {
        count: u32,
    }
    
    impl Counter {
        fn new() -> Counter {
            Counter { count: 0 }
        }
    }
    
    impl Iterator for Counter {
        type Item = u32;
    
        fn next(&mut self) -> Option<Self::Item> {
            self.count += 1;
            if self.count <= 5 {
                Some(self.count)
            } else {
                None
            }
        }
    }
    
    fn main() {
        let mut counter = Counter::new();
        while let Some(value) = counter.next() {
            println!("{}", value); // Output: 1, 2, 3, 4, 5
        }
    }

    21. What is the Cow type in Rust?

    Answer:
    Cow (Copy On Write) is a smart pointer that can clone the data if it needs to be mutated, avoiding unnecessary cloning. It is used to handle both borrowed and owned data efficiently.

    use std::borrow::Cow;
    
    fn modify_string(input: &str) -> Cow<str> {
        if input.contains("foo") {
            let mut owned = input.to_owned();
            owned.push_str("bar");
            Cow::Owned(owned)
        } else {
            Cow::Borrowed(input)
        }
    }
    
    fn main() {
        let s = "foobar";
        let result = modify_string(s);
        println!("{}", result); // Output: foobarbar
    }

    22. How do you define and use enums in Rust?

    Answer:
    Enums in Rust allow you to define a type by enumerating its possible variants.

    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }
    
    fn process_message(msg: Message) {
        match msg {
            Message::Quit => println!("Quit"),
            Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
            Message::Write(text) => println!("Text message: {}", text),
            Message::ChangeColor(r, g, b) => println!("Change color to red: {}, green: {}, blue: {}", r, g, b),
        }
    }
    
    fn main() {
        let msg = Message::Move { x: 10, y: 20 };
        process_message(msg); // Output: Move to x: 10, y: 20
    }

    23. What are Rust’s smart pointers and how do you use them?

    Answer:
    Rust has several smart pointers:

    • Box<T>: Used for heap allocation of single ownership.
      let boxed = Box::new(5);
      println!("{}", boxed); // Output: 5
    • Rc<T> (Reference Counted): Used for shared ownership in single-threaded contexts.
      use std::rc::Rc;
    
      let rc_value = Rc::new(5);
      let rc_value_clone = Rc::clone(&rc_value);
      println!("{}", Rc::strong_count(&rc_value)); // Output: 2
    • Arc<T> (Atomic Reference Counted): Used for shared ownership in multi-threaded contexts.
      use std::sync::Arc;
      use std::thread;
    
      let arc_value = Arc::new(5);
      let arc_value_clone = Arc::clone(&arc_value);
    
      let handle = thread::spawn(move || {
          println!("{}", arc_value_clone);
      });
    
      handle.join().unwrap();
    • RefCell<T>: Allows mutable borrowing checked at runtime.
      use std::cell::RefCell;
    
      let ref_cell = RefCell::new(5);
      *ref_cell.borrow_mut() += 1;
      println!("{}", ref_cell.borrow()); // Output: 6

    24. How do you handle asynchronous programming in Rust?

    Answer:
    Rust handles asynchronous programming using the async and await keywords, along with the Future trait.

    use tokio::time::{sleep, Duration};
    
    async fn async_function() {
        println!("Hello");
        sleep(Duration::from_secs(1)).await;
        println!("World");
    }
    
    #[tokio::main]
    async fn main() {
        async_function().await;
    }

    25. What is Rust’s approach to immutability and mutability?

    Answer:

    1. Immutability by Default:

    • In Rust, variables are immutable by default. This means once a variable is assigned a value, it cannot be changed.
    • This default behavior encourages safer code by preventing unintended modifications, leading to fewer bugs related to state changes.
       let x = 5;
       x = 6; // This will cause a compile-time error

    2. Explicit Mutability:

    • To make a variable mutable, you must explicitly use the mut keyword. This makes it clear to anyone reading the code that the variable’s value can be changed.
       let mut y = 5;
       y = 6; // This is allowed because y is mutable

    3. Borrowing and References:

    • Rust’s borrowing system distinguishes between immutable and mutable references.
    • You can have multiple immutable references (&T) to a value, but only one mutable reference (&mut T) at a time. This ensures data race safety at compile time.
       let mut z = 10;
       let r1 = &z; // Immutable borrow
       let r2 = &z; // Another immutable borrow, allowed
       let r3 = &mut z; // This will cause a compile-time error due to conflicting mutable and immutable borrows

    4. Scoped Mutability:

    • Rust’s mutable references are scoped. Once the scope of a mutable reference ends, other references can be created safely.
       let mut w = 20;
       {
           let r4 = &mut w; // Mutable borrow within this scope
           *r4 += 1;
       }
       // r4 is out of scope here, so it's safe to create another reference
       let r5 = &w; // Immutable borrow, allowed now

    5. Interior Mutability:

    • Rust provides the Cell and RefCell types for cases where you need to mutate data even when the outer type is immutable. This is called interior mutability and uses Rust’s borrowing rules to ensure safety at runtime.
       use std::cell::RefCell;
    
       let data = RefCell::new(5);
       *data.borrow_mut() += 1; // Mutable borrow within RefCell

    Key Takeaways

    • Rust’s default to immutability promotes safe and predictable code.
    • Explicit mutability with mut makes the intent clear.
    • The borrowing system ensures safe concurrency by enforcing borrowing rules at compile time.
    • Interior mutability allows controlled mutation in otherwise immutable contexts, providing flexibility without sacrificing safety.
    Scroll to Top