Rust Interview Preparation: Commonly Asked Questions – Set 1

Table of Contents

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