Rust Interview Preparation: Commonly Asked Questions – Set 1

Introduction

This comprehensive guide covers the most frequently asked Rust interview questions, ranging from basic concepts to advanced topics. Each question includes detailed explanations, code examples, and best practices to help you ace your Rust interview.

Table of Contents

  1. Ownership and Borrowing
  2. Lifetimes
  3. Traits and Generics
  4. Error Handling
  5. Concurrency
  6. Memory Management
  7. Common Collections
  8. Smart Pointers
  9. Pattern Matching
  10. Advanced Concepts

1. Ownership and Borrowing

Q1: Explain Rust's ownership rules and how they prevent memory bugs.

Answer: Rust's ownership system has three main rules:

  1. Each value has a single owner at any time
  2. When the owner goes out of scope, the value is dropped
  3. You can have either one mutable reference or multiple immutable references, but not both simultaneously
fn ownership_example() { // Rule 1: s owns the String let s = String::from("hello"); // Ownership moves to s2 let s2 = s; // println!("{}", s); // Error: s no longer valid // Rule 2: s2 goes out of scope here, memory freed // Rule 3: Borrowing rules let mut s3 = String::from("world"); let r1 = &s3; // Immutable borrow let r2 = &s3; // Another immutable borrow - OK // let r3 = &mut s3; // Error: can't borrow mutably with immutable borrows println!("{} and {}", r1, r2); let r4 = &mut s3; // Mutable borrow - OK (no other borrows active) r4.push_str("!"); }

Key Points:

  • Prevents data races at compile time
  • Eliminates dangling pointers
  • No garbage collection overhead
  • Zero-cost abstractions

Q2: What's the difference between passing by value, reference, and mutable reference?

Answer:

fn main() { let x = 5; let mut y = 10; let s = String::from("hello"); // Pass by value (copy for Copy types) take_copy(x); println!("x still usable: {}", x); // Works because i32 is Copy // Pass by value (move for non-Copy types) take_ownership(s); // println!("{}", s); // Error: s moved // Pass by immutable reference let s2 = String::from("world"); borrow(&s2); println!("s2 still usable: {}", s2); // Works // Pass by mutable reference change(&mut y); println!("y changed: {}", y); } fn take_copy(x: i32) { println!("Got copy: {}", x); } fn take_ownership(s: String) { println!("Took ownership of: {}", s); } // s dropped here fn borrow(s: &String) { println!("Borrowed: {}", s); } fn change(x: &mut i32) { *x += 10; }

Key Differences:

  • Value: Ownership transfers (or copy for Copy types)
  • Immutable reference: Borrow read-only access
  • Mutable reference: Borrow read-write access

Q3: Explain the concept of "move" semantics in Rust.

Answer:

#[derive(Debug)] struct Person { name: String, age: u32, } fn main() { // Move with non-Copy types let p1 = Person { name: String::from("Alice"), age: 30, }; // p1 is MOVED to p2, not copied let p2 = p1; // println!("{:?}", p1); // Error: p1 no longer valid println!("{:?}", p2); // OK // Move in function calls let s = String::from("hello"); takes_ownership(s); // println!("{}", s); // Error: s moved // Move in closures let v = vec![1, 2, 3]; let closure = move || { println!("Vector moved into closure: {:?}", v); }; closure(); // println!("{:?}", v); // Error: v moved into closure // Move with Copy types (copy, not move) let x = 5; let y = x; // Copy, not move println!("x: {}, y: {}", x, y); // Both work } fn takes_ownership(s: String) { println!("Took ownership of: {}", s); } // s dropped here

2. Lifetimes

Q4: What are lifetimes in Rust and why are they needed?

Answer: Lifetimes ensure that references are always valid (no dangling references). They're part of Rust's compile-time memory safety guarantees.

// Explicit lifetime annotation fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // Lifetime in structs struct ImportantExcerpt<'a> { part: &'a str, } // Multiple lifetimes fn complex<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { x } fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); println!("Longest: {}", result); // OK here } // println!("Longest: {}", result); // Error if result might refer to string2 // Struct with lifetime let novel = String::from("Call me Ishmael..."); let first_sentence = novel.split('.').next().unwrap(); let excerpt = ImportantExcerpt { part: first_sentence, }; // excerpt's lifetime tied to novel }

Lifetime Elision Rules:

  1. Each parameter gets its own lifetime
  2. If only one input lifetime, output gets that lifetime
  3. If &self or &mut self, output gets self's lifetime

Q5: Explain the 'static lifetime.

Answer: 'static lifetime means the reference is valid for the entire program duration.

// String literals have 'static lifetime let s: &'static str = "I live forever"; // Static variables static GLOBAL: i32 = 42; // Static with lifetime fn returns_static() -> &'static str { "This string is stored in the binary" } // Using 'static with generics fn print_if_static<T: 'static>(t: T) { // T must not contain any non-static references println!("Got static type"); } // Common misconception fn main() { let s = "hello"; // 'static let s2 = String::from("hello").as_str(); // println!("{}", s2); // Not 'static, lives only in this scope // 'static constraints in generics let v: Vec<i32> = vec![1, 2, 3]; print_if_static(v); // OK - i32 is 'static }

3. Traits and Generics

Q6: Explain traits and how they differ from interfaces in other languages.

Answer: Traits define shared behavior, similar to interfaces but more powerful.

// Define a trait trait Summary { fn summarize(&self) -> String; // Default implementation fn summarize_author(&self) -> String { String::from("(Unknown author)") } } // Implement trait for a type struct NewsArticle { headline: String, location: String, author: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } struct Tweet { username: String, content: String, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } // Trait as parameter fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); } // Trait bound syntax fn notify_bound<T: Summary>(item: &T) { println!("Breaking news! {}", item.summarize()); } // Multiple trait bounds use std::fmt::Display; fn notify_multi<T: Summary + Display>(item: &T) {} // Where clause fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + std::fmt::Debug, { 42 } // Returning impl Trait fn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from("of course"), } } // Trait objects (dynamic dispatch) fn returns_dynamic(choice: bool) -> Box<dyn Summary> { if choice { Box::new(NewsArticle { headline: String::from("Headline"), location: String::from("Location"), author: String::from("Author"), }) } else { Box::new(Tweet { username: String::from("user"), content: String::from("content"), }) } }

Key Differences from Interfaces:

  • Can provide default implementations
  • Can have associated types
  • Can be implemented for external types
  • Support static dispatch (monomorphization) by default

Q7: What are associated types in traits?

Answer: Associated types allow traits to have placeholder types that are specified when implementing.

// Iterator trait uses associated type trait Iterator { type Item; // Associated type fn next(&mut self) -> Option<Self::Item>; } struct Counter { count: u32, } impl Iterator for Counter { type Item = u32; // Specify the associated type fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } } // Another example: Container trait trait Container { type Item; // Associated type fn add(&mut self, item: Self::Item); fn get(&self, index: usize) -> Option<&Self::Item>; } struct MyVec<T> { items: Vec<T>, } impl<T> Container for MyVec<T> { type Item = T; fn add(&mut self, item: T) { self.items.push(item); } fn get(&self, index: usize) -> Option<&T> { self.items.get(index) } } fn main() { let mut counter = Counter { count: 0 }; while let Some(n) = counter.next() { println!("{}", n); } }

Benefits:

  • Cleaner syntax than generic parameters
  • Ensures type consistency across methods
  • Better for single implementation per type

4. Error Handling

Q8: Compare and contrast Option and Result types.

Answer: Both are enums for handling absence of values or errors.

// Option - for possible absence // enum Option<T> { // Some(T), // None, // } // Result - for possible errors // enum Result<T, E> { // Ok(T), // Err(E), // } fn main() { // Option examples let some_value: Option<i32> = Some(5); let none_value: Option<i32> = None; // Safe unwrapping match some_value { Some(x) => println!("Got: {}", x), None => println!("Got nothing"), } // Combinators let doubled = some_value.map(|x| x * 2); let filtered = some_value.filter(|&x| x > 10); let or_default = none_value.unwrap_or(0); // Result examples let success: Result<i32, &str> = Ok(42); let failure: Result<i32, &str> = Err("Something went wrong"); // Error handling with Result match success { Ok(value) => println!("Success: {}", value), Err(e) => println!("Error: {}", e), } // ? operator for propagation fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> { let num = s.parse::<i32>()?; // Propagates error Ok(num * 2) } // Combining Option and Result let result: Result<Option<i32>, &str> = Ok(Some(5)); if let Ok(Some(x)) = result { println!("Got: {}", x); } }

When to use:

  • Option: Simple absence of value (no error information needed)
  • Result: Operation that can fail with error context

Q9: Explain the ? operator and when you can use it.

Answer: The ? operator propagates errors or None values, making error handling concise.

use std::fs::File; use std::io::{self, Read}; use std::num::ParseIntError; // Without ? operator fn read_file_verbose() -> Result<String, io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), } } // With ? operator fn read_file_concise() -> Result<String, io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } // Even more concise fn read_file_short() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } // With Option fn first_positive(numbers: Vec<i32>) -> Option<i32> { let first = numbers.get(0)?; // Returns None if index out of bounds if *first > 0 { Some(*first) } else { None } } // Chaining multiple operations fn process_data() -> Result<i32, ParseIntError> { let a = "10".parse::<i32>()?; let b = "20".parse::<i32>()?; let c = "30".parse::<i32>()?; Ok(a + b + c) } // Mixed error types (need conversion) fn mixed_errors() -> Result<i32, Box<dyn std::error::Error>> { let file = File::open("data.txt")?; // io::Error let num = "42".parse::<i32>()?; // ParseIntError Ok(num) } fn main() -> Result<(), Box<dyn std::error::Error>> { let result = read_file_concise()?; println!("File content: {}", result); if let Some(x) = first_positive(vec![-1, -2, 3]) { println!("First positive: {}", x); } Ok(()) }

Rules for ?:

  • Can only be used in functions returning Result or Option
  • Converts error types using From trait
  • Returns early on Err or None

5. Concurrency

Q10: How does Rust prevent data races at compile time?

Answer: Rust's ownership system and Send/Sync traits prevent data races.

use std::thread; use std::sync::{Arc, Mutex}; fn main() { // This won't compile - ownership prevents sharing let data = vec![1, 2, 3]; let handle = thread::spawn(|| { // data // Can't capture by default - ownership issues }); // Move ownership to thread let handle = thread::spawn(move || { println!("{:?}", data); // data moved here }); handle.join().unwrap(); // println!("{:?}", data); // Error: data moved // Using Arc for shared ownership let data = Arc::new(vec![1, 2, 3, 4, 5]); let mut handles = vec![]; for i in 0..5 { let data_clone = Arc::clone(&data); handles.push(thread::spawn(move || { println!("Thread {} sees: {:?}", i, data_clone); })); } for handle in handles { handle.join().unwrap(); } // Mutex for mutable shared data let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; })); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); } // Send and Sync traits fn send_sync_example() { // Send: Type can be transferred across threads // Sync: Type can be safely shared between threads // i32 implements both Send and Sync let x: i32 = 5; thread::spawn(move || { println!("{}", x); }); // Rc<T> is NOT Send (not thread-safe) // Arc<T> IS Send (thread-safe) // Mutex<T> IS Send + Sync }

Key Points:

  • Send: Type can be transferred across threads
  • Sync: Type can be safely shared between threads
  • Compiler prevents sending non-Send types between threads
  • Prevents data races at compile time

Q11: Explain the difference between Mutex<T> and RwLock<T>.

Answer: Both provide interior mutability across threads, but with different semantics.

use std::sync::{Arc, Mutex, RwLock}; use std::thread; fn mutex_example() { // Mutex: One thread can access at a time let data = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let data = Arc::clone(&data); handles.push(thread::spawn(move || { let mut value = data.lock().unwrap(); *value += 1; })); } for handle in handles { handle.join().unwrap(); } println!("Mutex result: {}", *data.lock().unwrap()); } fn rwlock_example() { // RwLock: Multiple readers OR single writer let data = Arc::new(RwLock::new(0)); let mut handles = vec![]; // Writers for _ in 0..3 { let data = Arc::clone(&data); handles.push(thread::spawn(move || { let mut write = data.write().unwrap(); *write += 1; println!("Writer updated to {}", *write); })); } // Readers for i in 0..5 { let data = Arc::clone(&data); handles.push(thread::spawn(move || { let read = data.read().unwrap(); println!("Reader {} sees: {}", i, *read); })); } for handle in handles { handle.join().unwrap(); } } fn main() { mutex_example(); rwlock_example(); } // Performance comparison fn performance_comparison() { use std::time::Instant; // Mutex with heavy read load let mutex_data = Arc::new(Mutex::new(0)); let rwlock_data = Arc::new(RwLock::new(0)); // RwLock usually better for read-heavy workloads // Mutex better for write-heavy or balanced workloads }

Key Differences:

  • Mutex: Simple, one thread at a time
  • RwLock: Multiple readers or single writer
  • Use RwLock for read-heavy workloads
  • Use Mutex for write-heavy or when simplicity preferred

Q12: What are channels and how do you use them?

Answer: Channels allow communication between threads through message passing.

use std::sync::mpsc; use std::thread; use std::time::Duration; fn basic_channel() { // Create a channel let (tx, rx) = mpsc::channel(); // Spawn producer thread thread::spawn(move || { let messages = vec![ String::from("Hello"), String::from("from"), String::from("the"), String::from("thread"), ]; for msg in messages { tx.send(msg).unwrap(); thread::sleep(Duration::from_millis(100)); } }); // Receive messages in main thread for received in rx { println!("Got: {}", received); } } fn multiple_producers() { let (tx, rx) = mpsc::channel(); // Clone the transmitter for multiple producers let tx1 = tx.clone(); thread::spawn(move || { tx1.send("Message from thread 1").unwrap(); }); let tx2 = tx.clone(); thread::spawn(move || { tx2.send("Message from thread 2").unwrap(); }); // Drop original tx so channel closes when all clones dropped drop(tx); for received in rx { println!("Received: {}", received); } } fn with_result_channel() { let (tx, rx) = mpsc::channel::<Result<i32, String>>(); thread::spawn(move || { for i in 0..5 { if i == 3 { tx.send(Err("Something went wrong".to_string())).unwrap(); } else { tx.send(Ok(i * 10)).unwrap(); } thread::sleep(Duration::from_millis(50)); } }); for result in rx { match result { Ok(val) => println!("Got: {}", val), Err(e) => println!("Error: {}", e), } } } fn sync_channel_example() { use std::sync::mpsc::sync_channel; // Synchronous channel with buffer size 3 let (tx, rx) = sync_channel(3); thread::spawn(move || { for i in 1..=10 { println!("Sending {}", i); tx.send(i).unwrap(); // Blocks if buffer is full println!("Sent {}", i); } }); thread::sleep(Duration::from_secs(1)); // Receive slowly for received in rx { println!("Received: {}", received); thread::sleep(Duration::from_millis(500)); } } fn main() { println!("Basic channel:"); basic_channel(); println!("\nMultiple producers:"); multiple_producers(); println!("\nResult channel:"); with_result_channel(); }

6. Memory Management

Q13: Explain the difference between stack and heap allocation in Rust.

Answer: Stack vs heap allocation with Rust's ownership system.

use std::mem; fn main() { // Stack allocation (fixed size, known at compile time) let x = 5; // i32 on stack let y = true; // bool on stack let z = 3.14; // f64 on stack let arr = [1, 2, 3]; // array on stack (size known) let tuple = (1, "hello"); // tuple on stack println!("Stack size of i32: {} bytes", mem::size_of::<i32>()); println!("Stack size of array[3]: {} bytes", mem::size_of::<[i32; 3]>()); // Heap allocation (dynamic size) let s = String::from("hello"); // String data on heap let v = vec![1, 2, 3, 4, 5]; // Vec data on heap let boxed = Box::new(100); // Box puts value on heap println!("Size of String (on stack): {} bytes", mem::size_of::<String>()); println!("Size of Vec (on stack): {} bytes", mem::size_of::<Vec<i32>>()); // Stack vs heap performance fn stack_performance() { let start = std::time::Instant::now(); for i in 0..1_000_000 { let x = i; // Stack allocation } println!("Stack allocation: {:?}", start.elapsed()); } fn heap_performance() { let start = std::time::Instant::now(); for i in 0..1_000_000 { let x = Box::new(i); // Heap allocation } println!("Heap allocation: {:?}", start.elapsed()); } stack_performance(); heap_performance(); } // Memory layout of custom types struct MyStruct { a: u8, // 1 byte b: u32, // 4 bytes (needs padding) c: u16, // 2 bytes } // Layout: a(1) + padding(3) + b(4) + c(2) + padding(2) = 12 bytes fn memory_layout() { println!("Size of MyStruct: {} bytes", mem::size_of::<MyStruct>()); println!("Alignment: {} bytes", mem::align_of::<MyStruct>()); }

Key Differences:

  • Stack: Fast, LIFO, fixed size, no garbage collection
  • Heap: Flexible, dynamic size, requires explicit management
  • Rust's ownership ensures heap memory is freed when owner goes out of scope

Q14: What is RAII and how does Rust implement it?

Answer: RAII (Resource Acquisition Is Initialization) binds resource lifetime to object lifetime.

use std::fs::File; use std::io::Write; // RAII with custom type struct DatabaseConnection { id: u32, connected: bool, } impl DatabaseConnection { fn new(id: u32) -> Self { println!("Opening database connection {}", id); DatabaseConnection { id, connected: true } } fn query(&self, sql: &str) { if self.connected { println!("Connection {}: {}", self.id, sql); } } } // Drop trait for cleanup impl Drop for DatabaseConnection { fn drop(&mut self) { println!("Closing database connection {}", self.id); self.connected = false; } } // RAII with files fn file_example() -> std::io::Result<()> { { // File automatically closes when going out of scope let mut file = File::create("test.txt")?; file.write_all(b"Hello, World!")?; println!("File written"); } // File closed here // The file is automatically flushed and closed Ok(()) } // RAII with mutex use std::sync::Mutex; fn mutex_raii() { let mutex = Mutex::new(0); { // MutexGuard implements Drop let mut guard = mutex.lock().unwrap(); *guard += 1; println!("Incremented inside scope"); } // Mutex automatically unlocked here // Can lock again let value = *mutex.lock().unwrap(); println!("Value: {}", value); } // Custom RAII type struct ScopedTimer { name: String, start: std::time::Instant, } impl ScopedTimer { fn new(name: &str) -> Self { println!("Starting: {}", name); ScopedTimer { name: name.to_string(), start: std::time::Instant::now(), } } } impl Drop for ScopedTimer { fn drop(&mut self) { let elapsed = self.start.elapsed(); println!("Finished: {} (took {:?})", self.name, elapsed); } } fn main() -> std::io::Result<()> { { let _timer = ScopedTimer::new("database operation"); // Connection automatically closed when out of scope let conn = DatabaseConnection::new(1); conn.query("SELECT * FROM users"); } // conn dropped here, timer dropped here file_example()?; mutex_raii(); Ok(()) }

Benefits:

  • Automatic resource cleanup
  • Exception safety
  • No manual close() or free() calls
  • Prevents resource leaks

7. Common Collections

Q15: Compare Vec<T>, VecDeque<T>, and LinkedList<T>.

Answer: Different sequence containers with different performance characteristics.

use std::collections::{VecDeque, LinkedList}; fn main() { // Vec<T> - Dynamic array let mut vec = Vec::new(); vec.push(1); vec.push(2); vec.push(3); // Fast: O(1) index access let first = vec[0]; // Fast: O(1) push/pop at end vec.push(4); let last = vec.pop(); // Slow: O(n) insert/remove in middle vec.insert(1, 5); vec.remove(2); // VecDeque<T> - Double-ended queue let mut deque = VecDeque::new(); // Fast: O(1) push/pop at both ends deque.push_back(1); deque.push_back(2); deque.push_front(0); deque.push_front(-1); let front = deque.pop_front(); // -1 let back = deque.pop_back(); // 2 println!("Deque: {:?}", deque); // [0, 1] // LinkedList<T> - Doubly linked list let mut list = LinkedList::new(); list.push_back(1); list.push_back(2); list.push_front(0); // Fast: O(1) insert/remove at ends // Slow: O(n) index access // Better for: frequent splits/merges // Performance comparison fn benchmark() { use std::time::Instant; let size = 100_000; // Vec push back let start = Instant::now(); let mut vec = Vec::with_capacity(size); for i in 0..size { vec.push(i); } println!("Vec push back: {:?}", start.elapsed()); // VecDeque push back let start = Instant::now(); let mut deque = VecDeque::with_capacity(size); for i in 0..size { deque.push_back(i); } println!("VecDeque push back: {:?}", start.elapsed()); // VecDeque push front (requires shifting) let start = Instant::now(); let mut deque = VecDeque::with_capacity(size); for i in 0..size { deque.push_front(i); // More expensive than push_back } println!("VecDeque push front: {:?}", start.elapsed()); // LinkedList push back let start = Instant::now(); let mut list = LinkedList::new(); for i in 0..size { list.push_back(i); } println!("LinkedList push back: {:?}", start.elapsed()); } benchmark(); } // When to use each: // - Vec: Most cases, need fast index access // - VecDeque: Need queue/deque operations at both ends // - LinkedList: Rarely needed, only when splitting/merging frequently

Q16: How do you choose between HashMap and BTreeMap?

Answer: Based on ordering requirements and performance characteristics.

use std::collections::{HashMap, BTreeMap}; fn main() { // HashMap - Unordered, average O(1) operations let mut hashmap = HashMap::new(); hashmap.insert("apple", 3); hashmap.insert("banana", 2); hashmap.insert("cherry", 5); println!("HashMap (order not guaranteed): {:?}", hashmap); // BTreeMap - Sorted, O(log n) operations let mut btmap = BTreeMap::new(); btmap.insert("apple", 3); btmap.insert("banana", 2); btmap.insert("cherry", 5); println!("BTreeMap (sorted): {:?}", btmap); // BTreeMap range queries let range: Vec<_> = btmap.range("a"..="c").collect(); println!("Range 'a' to 'c': {:?}", range); // First and last entries if let Some((key, value)) = btmap.first_key_value() { println!("First: {}: {}", key, value); } if let Some((key, value)) = btmap.last_key_value() { println!("Last: {}: {}", key, value); } // Performance comparison fn benchmark() { use std::time::Instant; let size = 100_000; // HashMap insertion let start = Instant::now(); let mut hashmap = HashMap::with_capacity(size); for i in 0..size { hashmap.insert(i, i * 2); } println!("HashMap insertion: {:?}", start.elapsed()); // BTreeMap insertion let start = Instant::now(); let mut btmap = BTreeMap::new(); for i in 0..size { btmap.insert(i, i * 2); } println!("BTreeMap insertion: {:?}", start.elapsed()); // HashMap lookup let start = Instant::now(); for i in 0..size { let _ = hashmap.get(&i); } println!("HashMap lookup: {:?}", start.elapsed()); // BTreeMap lookup let start = Instant::now(); for i in 0..size { let _ = btmap.get(&i); } println!("BTreeMap lookup: {:?}", start.elapsed()); } benchmark(); } // When to use: // HashMap: // - Need fast lookups (O(1) average) // - Order doesn't matter // - Keys are hashable // // BTreeMap: // - Need sorted data // - Need range queries // - Order matters // - When consistent iteration order is important

8. Smart Pointers

Q17: Explain Box<T>, Rc<T>, and RefCell<T>.

Answer: Smart pointers with different ownership and mutability semantics.

use std::rc::Rc; use std::cell::RefCell; // Box<T> - Single ownership, heap allocation fn box_example() { // Box for heap allocation let b = Box::new(5); println!("b = {}", b); // Box for recursive types #[derive(Debug)] enum List { Cons(i32, Box<List>), Nil, } let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("{:?}", list); } // Rc<T> - Reference counting (single-threaded) fn rc_example() { use List2::{Cons, Nil}; #[derive(Debug)] enum List2 { Cons(i32, Rc<List2>), Nil, } let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out = {}", Rc::strong_count(&a)); } // RefCell<T> - Interior mutability (single-threaded) fn refcell_example() { let cell = RefCell::new(5); // Borrow mutably at runtime *cell.borrow_mut() += 1; println!("cell: {:?}", cell); // Multiple borrows checked at runtime { let r1 = cell.borrow(); let r2 = cell.borrow(); // let r3 = cell.borrow_mut(); // This would panic at runtime println!("r1: {}, r2: {}", r1, r2); } // Example with mock objects #[derive(Debug)] struct MockMessenger { sent_messages: RefCell<Vec<String>>, } impl MockMessenger { fn new() -> Self { MockMessenger { sent_messages: RefCell::new(vec![]), } } fn send(&self, message: &str) { self.sent_messages.borrow_mut().push(String::from(message)); } fn messages(&self) -> usize { self.sent_messages.borrow().len() } } let mock = MockMessenger::new(); mock.send("Hello"); mock.send("World"); println!("Messages: {}", mock.messages()); println!("{:?}", mock.sent_messages.borrow()); } // Combining Rc and RefCell for shared mutable data fn rc_refcell_example() { use std::rc::Rc; use std::cell::RefCell; let shared = Rc::new(RefCell::new(vec![1, 2, 3])); let a = Rc::clone(&shared); let b = Rc::clone(&shared); *a.borrow_mut() = vec![4, 5, 6]; println!("shared: {:?}", shared.borrow()); println!("b: {:?}", b.borrow()); } fn main() { println!("Box example:"); box_example(); println!("\nRc example:"); rc_example(); println!("\nRefCell example:"); refcell_example(); println!("\nRc + RefCell example:"); rc_refcell_example(); }

Key Differences:

  • Box: Single owner, heap allocation
  • Rc: Multiple owners (read-only), reference counted
  • RefCell: Single owner, mutable borrows checked at runtime
  • Rc>: Multiple owners with mutability

Q18: What's the difference between Cell and RefCell?

Answer: Both provide interior mutability but with different capabilities.

use std::cell::{Cell, RefCell}; // Cell<T> - Works with Copy types fn cell_example() { let cell = Cell::new(5); // Get and set values println!("Value: {}", cell.get()); cell.set(10); println!("After set: {}", cell.get()); // Update with closure cell.update(|x| x * 2); println!("After update: {}", cell.get()); // Replace let old = cell.replace(20); println!("Old: {}, New: {}", old, cell.get()); // Works with Copy types only // let string_cell = Cell::new(String::from("hello")); // Error: String not Copy } // RefCell<T> - Works with any type, provides references fn refcell_example() { let refcell = RefCell::new(vec![1, 2, 3]); // Borrow immutably { let borrowed = refcell.borrow(); println!("Borrowed: {:?}", borrowed); } // Borrow mutably { let mut borrowed_mut = refcell.borrow_mut(); borrowed_mut.push(4); borrowed_mut.push(5); } println!("After mutable borrow: {:?}", refcell.borrow()); // Try borrow while borrowed let r1 = refcell.borrow(); // let r2 = refcell.borrow_mut(); // This would panic at runtime drop(r1); // Drop before trying mutable borrow let mut r2 = refcell.borrow_mut(); // Now it's fine r2.push(6); } // Practical example: Counter struct Counter { value: Cell<i32>, } impl Counter { fn new() -> Self { Counter { value: Cell::new(0), } } fn increment(&self) { self.value.set(self.value.get() + 1); } fn get(&self) -> i32 { self.value.get() } } // Practical example: Graph with RefCell use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } impl Node { fn new(value: i32) -> Self { Node { value, children: RefCell::new(vec![]), } } fn add_child(&self, child: Rc<Node>) { self.children.borrow_mut().push(child); } } fn main() { println!("Cell example:"); cell_example(); println!("\nRefCell example:"); refcell_example(); // Counter with Cell let counter = Counter::new(); counter.increment(); counter.increment(); println!("Counter: {}", counter.get()); // Graph with RefCell let root = Rc::new(Node::new(1)); let child1 = Rc::new(Node::new(2)); let child2 = Rc::new(Node::new(3)); root.add_child(child1.clone()); root.add_child(child2.clone()); println!("Root: {:?}", root); }

Key Differences:

  • Cell: Copy types only, no references, O(1) operations
  • RefCell: Any type, provides references, runtime borrow checking
  • Use Cell for simple Copy types
  • Use RefCell when you need references or non-Copy types

9. Pattern Matching

Q19: Explain the different patterns available in Rust.

Answer: Rust provides rich pattern matching capabilities.

fn main() { // 1. Literal patterns let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } // 2. Named variable patterns match x { y => println!("Matched: {}", y), // y binds to value } // 3. Multiple patterns match x { 1 | 2 => println!("one or two"), 3 | 4 => println!("three or four"), _ => println!("something else"), } // 4. Range patterns let score = 85; match score { 0..=59 => println!("F"), 60..=69 => println!("D"), 70..=79 => println!("C"), 80..=89 => println!("B"), 90..=100 => println!("A"), _ => println!("Invalid"), } // 5. Struct patterns struct Point { x: i32, y: i32, } let p = Point { x: 10, y: 20 }; match p { Point { x, y } => println!("({}, {})", x, y), } match p { Point { x: 0, y } => println!("On y-axis at {}", y), Point { x, y: 0 } => println!("On x-axis at {}", x), Point { x, y } => println!("({}, {})", x, y), } // 6. Tuple patterns let tuple = (1, 2, 3); match tuple { (0, y, z) => println!("First is 0: {}, {}", y, z), (1, ..) => println!("First is 1, ignore rest"), _ => println!("Something else"), } // 7. Enum patterns enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } let msg = Message::Move { x: 10, y: 20 }; match msg { Message::Quit => println!("Quit"), Message::Move { x, y } => println!("Move to ({}, {})", x, y), Message::Write(text) => println!("Text: {}", text), Message::ChangeColor(r, g, b) => println!("Color: {}, {}, {}", r, g, b), } // 8. Reference patterns let x = 5; let y = &x; match y { &val => println!("Got value: {}", val), } // 9. @ bindings let id = 5; match id { e @ 1..=5 => println!("Small: {}", e), e @ 6..=10 => println!("Medium: {}", e), e => println!("Large: {}", e), } // 10. Guard patterns let pair = (2, -2); match pair { (x, y) if x == y => println!("Equal"), (x, y) if x + y == 0 => println!("Opposites"), (x, _) if x % 2 == 0 => println!("First is even"), _ => println!("No match"), } // 11. Destructuring nested structures struct Person { name: String, age: u32, address: Address, } struct Address { city: String, country: String, } let person = Person { name: "Alice".to_string(), age: 30, address: Address { city: "New York".to_string(), country: "USA".to_string(), }, }; match person { Person { name, age: 30, address: Address { city, .. }, } => println!("{} is 30 and lives in {}", name, city), _ => println!("Not matching"), } }

Q20: What are match guards and when would you use them?

Answer: Match guards add additional conditions to patterns.

fn main() { // Basic match guard let num = Some(4); match num { Some(x) if x < 5 => println!("Less than 5: {}", x), Some(x) if x == 5 => println!("Equal to 5"), Some(x) => println!("Greater than 5: {}", x), None => println!("None"), } // Multiple conditions let pair = (2, 4); match pair { (x, y) if x == y => println!("Equal"), (x, y) if x + y == 6 => println!("Sum is 6"), (x, y) if x * y == 8 => println!("Product is 8"), _ => println!("No match"), } // Using external variables let threshold = 10; let value = 15; match value { x if x > threshold => println!("Above threshold"), x if x == threshold => println!("At threshold"), _ => println!("Below threshold"), } // Complex conditions struct Person { name: String, age: u32, has_license: bool, } let person = Person { name: "Bob".to_string(), age: 17, has_license: false, }; match person { Person { age, has_license, .. } if age >= 16 && has_license => { println!("Can drive"); } Person { age, .. } if age >= 16 => { println!("Old enough but no license"); } _ => println!("Can't drive"), } // Guards with enums enum Temperature { Celsius(f64), Fahrenheit(f64), } let temp = Temperature::Celsius(25.0); match temp { Temperature::Celsius(c) if c <= 0 => println!("Freezing"), Temperature::Celsius(c) if c >= 100 => println!("Boiling"), Temperature::Celsius(c) => println!("Normal temp: {}°C", c), Temperature::Fahrenheit(f) if f <= 32 => println!("Freezing"), Temperature::Fahrenheit(f) if f >= 212 => println!("Boiling"), Temperature::Fahrenheit(f) => println!("Normal temp: {}°F", f), } // Guards with multiple patterns let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), // Guard applies to all patterns _ => println!("no"), } // This is equivalent to (4 | 5 | 6) if y }

10. Advanced Concepts

Q21: Explain the difference between impl Trait and trait objects.

Answer: Static dispatch vs dynamic dispatch.

use std::fmt::Display; // impl Trait (static dispatch) fn returns_impl_trait() -> impl Display { // Can only return one concrete type format!("Hello, World!") } // Can't do this with impl Trait: // fn returns_conditional(flag: bool) -> impl Display { // if flag { // "String literal" // &str // } else { // 42 // i32 - Error: different types // } // } // Trait objects (dynamic dispatch) fn returns_trait_object(flag: bool) -> Box<dyn Display> { if flag { Box::new("String literal") } else { Box::new(42) } } // impl Trait as parameter fn print_impl_trait(item: impl Display) { println!("{}", item); } // Trait object as parameter fn print_trait_object(item: &dyn Display) { println!("{}", item); } // Where clauses with both fn process<T: Display>(items: Vec<T>) -> impl Iterator<Item = T> { items.into_iter().map(|x| { println!("Processing: {}", x); x }) } // Performance comparison fn dispatch_comparison() { use std::time::Instant; // Static dispatch (monomorphized) fn static_dispatch<T: Display>(x: T) -> String { format!("{}", x) } // Dynamic dispatch (virtual call) fn dynamic_dispatch(x: &dyn Display) -> String { format!("{}", x) } let values: Vec<Box<dyn Display>> = vec![ Box::new(1), Box::new(2.5), Box::new("three"), ]; // Static dispatch - different function for each type for v in &values { // Can't use static dispatch here with mixed types } // Dynamic dispatch - works with mixed types for v in &values { println!("{}", dynamic_dispatch(&**v)); } } // Vtable layout trait Animal { fn make_sound(&self) -> String; } struct Dog; impl Animal for Dog { fn make_sound(&self) -> String { "Woof!".to_string() } } struct Cat; impl Animal for Cat { fn make_sound(&self) -> String { "Meow!".to_string() } } fn main() { // impl Trait returns let s = returns_impl_trait(); println!("{}", s); // Trait object returns let obj = returns_trait_object(true); println!("{}", obj); let obj = returns_trait_object(false); println!("{}", obj); // Vec of trait objects let animals: Vec<Box<dyn Animal>> = vec![ Box::new(Dog), Box::new(Cat), Box::new(Dog), ]; for animal in animals { println!("{}", animal.make_sound()); } }

Key Differences:

  • impl Trait: Static dispatch, single concrete type, no runtime overhead
  • dyn Trait: Dynamic dispatch, multiple types, vtable overhead
  • impl Trait can only appear in function parameters and return positions
  • dyn Trait requires pointer indirection (Box, &, etc.)

Q22: What are associated types and when would you use them?

Answer: Associated types define placeholder types in traits.

// Associated type in Iterator trait trait Iterator { type Item; // Associated type fn next(&mut self) -> Option<Self::Item>; } // Implementation with associated type struct Counter { count: u32, } impl Iterator for Counter { type Item = u32; // Specify the type fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } } // Generic vs Associated Type trait GenericContainer<T> { // Generic fn add(&mut self, item: T); fn get(&self, index: usize) -> Option<&T>; } trait AssocContainer { // Associated type type Item; fn add(&mut self, item: Self::Item); fn get(&self, index: usize) -> Option<&Self::Item>; } // Generic can be implemented multiple times struct MultiTypeContainer; impl GenericContainer<i32> for MultiTypeContainer { fn add(&mut self, item: i32) {} fn get(&self, index: usize) -> Option<&i32> { None } } impl GenericContainer<String> for MultiTypeContainer { fn add(&mut self, item: String) {} fn get(&self, index: usize) -> Option<&String> { None } } // Associated type can only be implemented once struct SingleTypeContainer; impl AssocContainer for SingleTypeContainer { type Item = i32; // Only one Item type per implementation fn add(&mut self, item: i32) {} fn get(&self, index: usize) -> Option<&i32> { None } } // Graph example trait Graph { type Node; type Edge; fn nodes(&self) -> Vec<Self::Node>; fn edges(&self, node: &Self::Node) -> Vec<Self::Edge>; fn connect(&mut self, from: Self::Node, to: Self::Node, edge: Self::Edge); } struct SimpleGraph { nodes: Vec<u32>, edges: Vec<(u32, u32, String)>, } impl Graph for SimpleGraph { type Node = u32; type Edge = String; fn nodes(&self) -> Vec<Self::Node> { self.nodes.clone() } fn edges(&self, node: &Self::Node) -> Vec<Self::Edge> { self.edges .iter() .filter(|(from, to, _)| from == node || to == node) .map(|(_, _, edge)| edge.clone()) .collect() } fn connect(&mut self, from: Self::Node, to: Self::Node, edge: Self::Edge) { self.edges.push((from, to, edge)); } } fn main() { let mut counter = Counter { count: 0 }; while let Some(n) = counter.next() { println!("{}", n); } // Graph usage let mut graph = SimpleGraph { nodes: vec![1, 2, 3], edges: vec![], }; graph.connect(1, 2, "edge1".to_string()); println!("Edges from 1: {:?}", graph.edges(&1)); }

Benefits of Associated Types:

  • Cleaner API (one type per trait)
  • Type relationships are clearer
  • Prevents multiple implementations for same type
  • Better for type inference

Q23: Explain the Deref trait and deref coercion.

Answer: Deref allows smart pointers to behave like references.

use std::ops::Deref; // Custom smart pointer struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } // Implement Deref impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } // Deref coercion example fn hello(name: &str) { println!("Hello, {}!", name); } fn main() { // Basic dereferencing let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); // *(y.deref()) // Deref coercion let m = MyBox::new(String::from("Rust")); // &MyBox<String> -> &String (via Deref) -> &str (via String's Deref) hello(&m); // Works due to deref coercion // Without deref coercion, would need: hello(&(*m)[..]); // Multiple deref steps struct Person { name: String, } impl Deref for Person { type Target = String; fn deref(&self) -> &String { &self.name } } let person = Person { name: String::from("Alice"), }; // Person -> String -> str hello(&person); // Mutable deref use std::ops::DerefMut; impl<T> DerefMut for MyBox<T> { fn deref_mut(&mut self) -> &mut T { &mut self.0 } } let mut mybox = MyBox::new(String::from("hello")); *mybox = String::from("world"); // Method calls with deref coercion let s = MyBox::new(String::from("hello")); // len() comes from String, not MyBox println!("Length: {}", s.len()); // Equivalent to: println!("Length: {}", (*s).len()); } // Common Deref implementations fn common_derefs() { // Vec implements Deref to slice let v = vec![1, 2, 3, 4, 5]; let slice: &[i32] = &v; // Deref coercion // String implements Deref to str let s = String::from("hello"); let str_slice: &str = &s; // Deref coercion // Box implements Deref to its contents let b = Box::new(42); let int_ref: &i32 = &b; // Deref coercion }

Deref Coercion Rules:

  • Converts &T to &U when T: Deref<Target=U>
  • Happens automatically in function arguments
  • Works for method calls
  • Multiple deref steps allowed

Q24: What are closures and how do they capture variables?

Answer: Closures are anonymous functions that can capture variables from their environment.

fn main() { // Basic closure let add_one = |x: i32| -> i32 { x + 1 }; let add_one = |x| x + 1; // Type inference println!("{}", add_one(5)); // Capture by reference let x = vec![1, 2, 3]; let print_vec = || println!("{:?}", x); // Captures &x print_vec(); println!("Still have: {:?}", x); // x still usable // Capture by mutable reference let mut y = 5; let mut inc = || y += 1; // Captures &mut y inc(); println!("y = {}", y); // 6 // Capture by value (move) let z = String::from("hello"); let take = move || { // Moves z into closure println!("{}", z); }; take(); // println!("{}", z); // Error: z moved // Closure traits fn call_once<F>(f: F) where F: FnOnce() { f(); } fn call_mut<F>(mut f: F) where F: FnMut() { f(); f(); } fn call_fn<F>(f: F) where F: Fn() { f(); f(); } let mut count = 0; // FnOnce - can be called once let once = || { println!("Once"); // Can't capture mutably }; // FnMut - can mutate captured variables let mut mut_closure = || { count += 1; println!("Count: {}", count); }; // Fn - doesn't mutate captured variables let immut_closure = || { println!("Count: {}", count); }; call_once(once); call_mut(mut_closure); call_fn(immut_closure); // Closure as return value fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y // Need move to capture x } let add_five = make_adder(5); println!("{}", add_five(3)); // 8 // Closure in collections let funcs: Vec<Box<dyn Fn(i32) -> i32>> = vec![ Box::new(|x| x * 2), Box::new(|x| x + 10), Box::new(|x| x * x), ]; for f in funcs { println!("{}", f(5)); } // Performance comparison fn bench_closures() { use std::time::Instant; let data: Vec<i32> = (0..1_000_000).collect(); // Closure let start = Instant::now(); let result: Vec<_> = data.iter().map(|&x| x * 2).collect(); println!("Closure: {:?}", start.elapsed()); // Function pointer fn double(x: &i32) -> i32 { x * 2 } let start = Instant::now(); let result: Vec<_> = data.iter().map(double).collect(); println!("Function pointer: {:?}", start.elapsed()); } }

Closure Capture Modes:

  • By reference (&T): Immutable borrow
  • By mutable reference (&mut T): Mutable borrow
  • By value (T): Takes ownership (with move keyword)

Q25: Explain the difference between Fn, FnMut, and FnOnce.

Answer: These traits define how closures capture and use variables.

fn main() { let mut data = vec![1, 2, 3]; let text = String::from("hello"); // FnOnce - consumes captured variables let consume = move || { println!("Consuming: {:?} and {}", data, text); // data and text moved here }; // Can only call once consume(); // consume(); // Error: moved // FnMut - can mutate captured variables let mut mutate = || { data.push(4); // data captured by &mut println!("Mutated: {:?}", data); }; // Can call multiple times mutate(); mutate(); // Fn - immutably borrows variables let borrow = || { println!("Borrowing: {:?} and {}", data, text); }; // Can call multiple times borrow(); borrow(); // Function that takes different closure types fn call_once<F>(f: F) where F: FnOnce() { f(); } fn call_mut<F>(mut f: F) where F: FnMut() { f(); f(); } fn call_fn<F>(f: F) where F: Fn() { f(); f(); } // FnOnce closure let s = String::from("once"); let once = move || println!("{}", s); call_once(once); // FnMut closure let mut count = 0; let mut mut_closure = || { count += 1; println!("Count: {}", count); }; call_mut(mut_closure); // Fn closure let x = 5; let fn_closure = || println!("x = {}", x); call_fn(fn_closure); // Real-world example: sort_by_key let mut people = vec![ ("Alice", 30), ("Bob", 25), ("Charlie", 35), ]; // Uses FnMut people.sort_by_key(|&(_, age)| age); println!("Sorted by age: {:?}", people); // Custom implementation fn custom_sort<T, F>(slice: &mut [T], mut key_fn: F) where F: FnMut(&T) -> i32, // FnMut can be called multiple times { // Simplified sorting for i in 0..slice.len() { for j in i+1..slice.len() { if key_fn(&slice[i]) > key_fn(&slice[j]) { slice.swap(i, j); } } } } let mut numbers = vec![5, 3, 1, 4, 2]; custom_sort(&mut numbers, |&x| x); println!("Sorted: {:?}", numbers); } // Implementing your own closure traits struct MyClosure { count: i32, } impl FnOnce<()> for MyClosure { type Output = i32; extern "rust-call" fn call_once(self, args: ()) -> i32 { self.count } } impl FnMut<()> for MyClosure { extern "rust-call" fn call_mut(&mut self, args: ()) -> i32 { self.count } } impl Fn<()> for MyClosure { extern "rust-call" fn call(&self, args: ()) -> i32 { self.count } }

Key Differences:

  • FnOnce: Can be called once, may consume captured values
  • FnMut: Can mutate captured state, can be called multiple times
  • Fn: Immutably borrows captured values, can be called multiple times

Interview Tips

Before the Interview:

  1. Review ownership rules - most important concept
  2. Practice writing code on paper or whiteboard
  3. Understand common patterns (Option, Result, iterators)
  4. Know the standard library (collections, concurrency primitives)

During the Interview:

  1. Explain your thought process while coding
  2. Discuss trade-offs between different approaches
  3. Mention error handling and edge cases
  4. Show understanding of performance implications

Common Mistakes to Avoid:

  • Forgetting semicolons in match arms
  • Not handling Option and Result properly
  • Confusing &str and String
  • Misunderstanding move semantics
  • Ignoring lifetime annotations when needed

Additional Resources:

Good luck with your Rust interview! Remember that understanding the concepts deeply is more important than memorizing syntax.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper