Rust Interview Preparation – Commonly Asked Questions Set 2

Table of Contents

  1. Ownership and Borrowing Deep Dive
  2. Lifetimes
  3. Traits and Generics
  4. Error Handling
  5. Concurrency
  6. Smart Pointers
  7. Memory Management
  8. Advanced Pattern Matching
  9. Unsafe Rust
  10. Performance Optimization
  11. Macros
  12. Async Programming
  13. Common Interview Problems

1. Ownership and Borrowing Deep Dive

Q1: Explain the ownership rules with examples of move semantics vs copy semantics

// Copy semantics (types that implement Copy trait) fn copy_example() { let x = 5; // i32 implements Copy let y = x; // x is copied, not moved println!("x = {}, y = {}", x, y); // Both valid let t = (1, 2); // Tuple of Copy types let u = t; // Copied println!("t = {:?}", t); // Still valid } // Move semantics (types without Copy) fn move_example() { let s1 = String::from("hello"); let s2 = s1; // s1 MOVED to s2 // println!("{}", s1); // ERROR: s1 no longer valid println!("{}", s2); // OK // Move in functions let s = String::from("world"); take_ownership(s); // s MOVED into function // println!("{}", s); // ERROR: s no longer valid // Return moves ownership back let s3 = String::from("hello"); let s4 = give_ownership(s3); // s3 moved, s4 gets ownership println!("{}", s4); } fn take_ownership(s: String) { println!("Got: {}", s); } // s dropped here fn give_ownership(s: String) -> String { s // ownership returned } // Partial moves fn partial_move() { struct Person { name: String, age: i32, } let person = Person { name: String::from("Alice"), age: 30, }; let name = person.name; // name moved out let age = person.age; // age copied (i32 is Copy) // println!("{:?}", person); // ERROR: person partially moved println!("{} is {}", name, age); }

Q2: Explain borrowing rules and common borrowing errors

fn borrowing_rules() { // Rule 1: At any time, you can have either one mutable reference // or any number of immutable references let mut s = String::from("hello"); let r1 = &s; // immutable borrow let r2 = &s; // another immutable borrow - OK println!("{} and {}", r1, r2); // r1 and r2 go out of scope here let r3 = &mut s; // mutable borrow - OK now println!("{}", r3); // ERROR: Cannot have mutable borrow while immutable exists let r4 = &s; // let r5 = &mut s; // ERROR: cannot borrow as mutable // Rule 2: References must always be valid // let reference_to_nothing = dangle(); // ERROR: returns reference to dropped value } // fn dangle() -> &String { // let s = String::from("hello"); // &s // s goes out of scope, reference would be dangling // } // Common borrowing patterns fn borrowing_patterns() { let mut data = vec![1, 2, 3, 4]; // Good: scoped borrows { let first = &data[0]; println!("First: {}", first); } // immutable borrow ends data.push(5); // OK now // Good: using different parts let slice = &mut data[..]; // borrow whole vector slice[0] = 10; // modify through slice // data.push(6); // ERROR: can't push while borrowed // NLL (Non-Lexical Lifetimes) example let mut v = vec![1, 2, 3]; let first = &v[0]; println!("{}", first); v.push(4); // This works in modern Rust (NLL) }

Q3: What is the difference between &str and String?

fn string_vs_str() { // String: owned, heap-allocated, mutable let mut owned = String::from("hello"); owned.push_str(" world"); owned.push('!'); println!("Owned: {}", owned); // &str: borrowed slice, immutable, can point to anywhere let literal: &str = "hello"; // points to binary let from_string: &str = &owned[..]; // points to heap let slice: &str = &owned[0..5]; // slice of String // Conversions let s: String = "hello".to_string(); let s2: String = String::from("world"); let str_slice: &str = &s; // String -> &str let new_string: String = str_slice.to_string(); // &str -> String // Function parameters - prefer &str for flexibility fn process(text: &str) { println!("Processing: {}", text); } process("literal"); process(&String::from("owned")); // Performance implications let s1 = String::from("hello"); let s2 = s1; // move, no allocation // let s3 = s1; // error let s4 = "hello".to_string(); // allocates let s5 = s4.clone(); // allocates again }

2. Lifetimes

Q4: Explain lifetimes and when they're needed

// Basic lifetime annotation fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // Lifetimes in structs struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention: {}", announcement); self.part // lifetime of self is elided } } // Multiple lifetimes fn complex<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { x // return must have lifetime of x } // Static lifetime fn static_lifetime() -> &'static str { "This string lives forever" // stored in binary } // Lifetime elision rules fn first_word(s: &str) -> &str { // lifetimes elided s.split_whitespace().next().unwrap() } fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(&string1, &string2); println!("Longest: {}", result); } // string2 dropped, but result still valid? No, if result points to string2 // println!("{}", result); // ERROR if result points to string2 // Struct with lifetime let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let excerpt = ImportantExcerpt { part: first_sentence, }; }

Q5: Explain lifetime elision rules

// Elision rules: // 1. Each elided lifetime in input becomes a distinct lifetime parameter // 2. If there's one input lifetime, all output lifetimes get that lifetime // 3. If there are multiple input lifetimes but one is &self or &mut self, // all output lifetimes get the lifetime of self // Rule 1 example: multiple input lifetimes fn multiply(x: &i32, y: &i32) -> i32 { // desugared: fn multiply<'a, 'b>(x: &'a i32, y: &'b i32) -> i32 *x * *y } // Rule 2 example: one input lifetime fn first_char(s: &str) -> &str { // desugared: fn first_char<'a>(s: &'a str) -> &'a str &s[0..1] } // Rule 3 example: methods with self struct Container { data: String, } impl Container { // desugared: fn get_data<'a>(&'a self) -> &'a str fn get_data(&self) -> &str { &self.data } // desugared: fn get_data_mut<'a>(&'a mut self) -> &'a mut String fn get_data_mut(&mut self) -> &mut String { &mut self.data } // Multiple inputs but one is self fn process<'a>(&'a self, other: &str) -> &'a str { self.get_data() } }

3. Traits and Generics

Q6: Explain trait objects vs generics

// Generic (static dispatch) fn generic_display<T: std::fmt::Display>(item: T) { println!("{}", item); } // Trait object (dynamic dispatch) fn trait_object_display(item: &dyn std::fmt::Display) { println!("{}", item); } // Trait object with Box fn boxed_trait(item: Box<dyn std::fmt::Display>) { println!("{}", item); } // Complex example trait Animal { fn make_sound(&self); } struct Dog; struct Cat; impl Animal for Dog { fn make_sound(&self) { println!("Woof!"); } } impl Animal for Cat { fn make_sound(&self) { println!("Meow!"); } } // Generic (static dispatch) - creates separate function for each type fn generic_animal<T: Animal>(animal: &T) { animal.make_sound(); } // Trait object (dynamic dispatch) - single function with vtable lookup fn trait_object_animal(animal: &dyn Animal) { animal.make_sound(); } // Vector of different types fn animal_sounds() { let animals: Vec<Box<dyn Animal>> = vec![ Box::new(Dog), Box::new(Cat), Box::new(Dog), ]; for animal in animals { animal.make_sound(); // dynamic dispatch } } // Performance comparison fn dispatch_comparison() { // Static dispatch - monomorphized, potentially faster, larger binary generic_animal(&Dog); generic_animal(&Cat); // Dynamic dispatch - single function, vtable overhead trait_object_animal(&Dog); trait_object_animal(&Cat); }

Q7: Explain common traits and when to implement them

use std::ops::{Add, Deref}; use std::fmt; // Display vs Debug #[derive(Debug)] struct Point { x: i32, y: i32, } impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } // Default trait #[derive(Default)] struct Config { host: String, port: u16, timeout: Option<u64>, } // Clone and Copy #[derive(Clone, Copy)] struct CopyType { x: i32, y: i32, } // PartialEq and Eq #[derive(PartialEq, Eq)] struct UserId(u32); // PartialOrd and Ord #[derive(PartialOrd, Ord, PartialEq, Eq)] struct Priority(u32); // Iterator struct Counter { count: u32, } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } } // From and Into struct Number { value: i32, } impl From<i32> for Number { fn from(value: i32) -> Self { Number { value } } } // Operator overloading #[derive(Debug, Clone, Copy)] struct Vector { x: i32, y: i32, } impl Add for Vector { type Output = Vector; fn add(self, other: Vector) -> Vector { Vector { x: self.x + other.x, y: self.y + other.y, } } } // Deref for smart pointers struct MyBox<T>(T); impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } fn main() { let point = Point { x: 10, y: 20 }; println!("Debug: {:?}", point); println!("Display: {}", point); let config = Config::default(); let sum = Vector { x: 1, y: 2 } + Vector { x: 3, y: 4 }; let num = Number::from(42); let mut counter = Counter { count: 0 }; while let Some(c) = counter.next() { println!("{}", c); } }

4. Error Handling

Q8: Compare Result vs Option vs panic!

use std::fs::File; use std::io::Read; // Option - for optional values fn find_user(name: &str) -> Option<&str> { let users = ["Alice", "Bob", "Charlie"]; users.iter().find(|&&user| user == name).copied() } // Result - for recoverable errors fn read_file(path: &str) -> Result<String, std::io::Error> { let mut file = File::open(path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } // Custom error type #[derive(Debug)] enum AppError { Io(std::io::Error), Parse(std::num::ParseIntError), NotFound(String), } impl From<std::io::Error> for AppError { fn from(err: std::io::Error) -> Self { AppError::Io(err) } } impl From<std::num::ParseIntError> for AppError { fn from(err: std::num::ParseIntError) -> Self { AppError::Parse(err) } } fn complex_operation() -> Result<i32, AppError> { let content = read_file("number.txt")?; let num = content.trim().parse::<i32>()?; Ok(num * 2) } // When to use panic! fn division(dividend: f64, divisor: f64) -> f64 { if divisor == 0.0 { panic!("Division by zero!"); } dividend / divisor } fn main() { // Option handling match find_user("Alice") { Some(user) => println!("Found: {}", user), None => println!("User not found"), } // Result handling match read_file("test.txt") { Ok(content) => println!("File: {}", content), Err(e) => println!("Error: {}", e), } // Combinators let result = find_user("Bob") .map(|s| s.to_uppercase()) .ok_or_else(|| AppError::NotFound("Bob".to_string())); // Unwrap/expect (use sparingly) let num = "42".parse::<i32>().expect("Invalid number"); // Custom error handling match complex_operation() { Ok(val) => println!("Result: {}", val), Err(AppError::Io(e)) => println!("IO error: {}", e), Err(AppError::Parse(e)) => println!("Parse error: {}", e), Err(AppError::NotFound(s)) => println!("Not found: {}", s), } }

Q9: Explain the ? operator and its usage

use std::fs; use std::io; use std::num::ParseIntError; // Without ? - verbose fn read_file_verbose() -> Result<i32, io::Error> { let result = fs::read_to_string("number.txt"); let content = match result { Ok(c) => c, Err(e) => return Err(e), }; let num = content.trim().parse::<i32>(); match num { Ok(n) => Ok(n), Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)), } } // With ? - concise fn read_file_concise() -> Result<i32, io::Error> { let content = fs::read_to_string("number.txt")?; let num = content.trim().parse::<i32>() .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; Ok(num * 2) } // ? with Option fn first_element_greater_than(vec: Vec<i32>, threshold: i32) -> Option<i32> { let first = vec.get(0)?; // Returns None if vec is empty if *first > threshold { Some(*first) } else { None } } // ? in main (Result return) fn main() -> Result<(), Box<dyn std::error::Error>> { let content = fs::read_to_string("config.txt")?; let lines: Vec<&str> = content.lines().collect(); for line in lines { println!("{}", line); } // Can use ? with custom error types let num = complex_operation()?; println!("Number: {}", num); Ok(()) } // Complex example with multiple error types use thiserror::Error; #[derive(Error, Debug)] enum MyError { #[error("IO error: {0}")] Io(#[from] io::Error), #[error("Parse error: {0}")] Parse(#[from] ParseIntError), #[error("Invalid data: {0}")] InvalidData(String), } fn process_data() -> Result<i32, MyError> { let content = fs::read_to_string("data.txt")?; // automatically converts to MyError let num = content.trim().parse::<i32>()?; // automatically converts Ok(num) }

5. Concurrency

Q10: Explain threads and how to share data between them

use std::thread; use std::sync::{Arc, Mutex, mpsc}; use std::time::Duration; // Basic thread spawning fn basic_threads() { let handle = thread::spawn(|| { for i in 1..10 { println!("Spawned thread: {}", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("Main thread: {}", i); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); // Wait for spawned thread } // Moving data into threads fn move_into_thread() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Vector: {:?}", v); }); // v moved into closure handle.join().unwrap(); // println!("{:?}", v); // ERROR: v moved } // Sharing data with Arc and Mutex fn shared_data() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); // 10 } // Channels for communication fn channels() { let (tx, rx) = mpsc::channel(); let tx2 = tx.clone(); thread::spawn(move || { let vals = vec![ String::from("hello"), String::from("from"), String::from("thread"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); thread::spawn(move || { let vals = vec![ String::from("more"), String::from("messages"), ]; for val in vals { tx2.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); for received in rx { println!("Got: {}", received); } } // Rayon for parallel iterators use rayon::prelude::*; fn parallel_iteration() { let numbers: Vec<i32> = (0..1000).collect(); // Sequential let sum_seq: i32 = numbers.iter().sum(); // Parallel let sum_par: i32 = numbers.par_iter().sum(); // Parallel map let squares: Vec<i32> = numbers.par_iter() .map(|&x| x * x) .collect(); println!("Sum: {}, {}", sum_seq, sum_par); } fn main() { basic_threads(); move_into_thread(); shared_data(); // channels(); // This would block parallel_iteration(); }

Q11: Explain Send and Sync traits

use std::thread; use std::sync::{Arc, Mutex, RwLock}; use std::rc::Rc; // Send: types that can be transferred across threads // Sync: types that can be referenced across threads // Rc is not Send or Sync fn rc_not_send() { let rc = Rc::new(5); // thread::spawn(move || { // println!("{}", rc); // ERROR: Rc<i32> cannot be sent between threads safely // }); } // Arc is Send + Sync fn arc_is_send_sync() { let arc = Arc::new(5); let handle = thread::spawn(move || { println!("{}", arc); // OK: Arc is Send }); handle.join().unwrap(); } // Mutex provides interior mutability across threads fn mutex_sync() { 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!("Counter: {}", *counter.lock().unwrap()); } // RwLock for multiple readers or single writer fn rwlock_example() { let data = Arc::new(RwLock::new(vec![1, 2, 3])); let readers: Vec<_> = (0..5).map(|i| { let data = Arc::clone(&data); thread::spawn(move || { let read = data.read().unwrap(); println!("Reader {} sees: {:?}", i, *read); }) }).collect(); let writer = thread::spawn(move || { let mut write = data.write().unwrap(); write.push(4); println!("Writer added 4"); }); for reader in readers { reader.join().unwrap(); } writer.join().unwrap(); } // Custom types and Send/Sync struct NotSend { data: Rc<i32>, // Rc makes the whole struct not Send } struct SendType { data: Arc<i32>, // Arc makes it Send } // Unsafe impl Send for NotSend {} // Would be unsafe to do this fn main() { arc_is_send_sync(); mutex_sync(); rwlock_example(); }

6. Smart Pointers

Q12: Compare Box, Rc, RefCell, and Arc

use std::rc::Rc; use std::cell::RefCell; use std::sync::{Arc, Mutex}; // Box<T> - for heap allocation, single ownership fn box_example() { // Stack allocation let stack_num = 5; // Heap allocation with Box let boxed_num = Box::new(5); println!("Boxed: {}", boxed_num); // 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 counted, multiple ownership (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!("Reference count after a: {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("Reference count after b: {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("Reference count after c: {}", Rc::strong_count(&a)); } println!("Reference count after c drops: {}", Rc::strong_count(&a)); } // RefCell<T> - interior mutability (single-threaded) fn refcell_example() { use std::cell::RefCell; // Normally can't mutate through immutable reference // let x = 5; // let y = &x; // *y = 10; // ERROR let cell = RefCell::new(5); // Borrow mutably at runtime (checked at runtime, not compile time) { let mut mut_ref = cell.borrow_mut(); *mut_ref += 1; } // mutable borrow ends here // Multiple immutable borrows allowed { let ref1 = cell.borrow(); let ref2 = cell.borrow(); println!("ref1: {}, ref2: {}", ref1, ref2); } // This would panic at runtime (not compile time) // let mut_ref = cell.borrow_mut(); // let ref1 = cell.borrow(); // PANIC: already mutably borrowed println!("Final value: {}", cell.borrow()); } // Arc<T> + Mutex<T> - thread-safe reference counting fn arc_example() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); handles.push(std::thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; })); } for handle in handles { handle.join().unwrap(); } println!("Counter: {}", *counter.lock().unwrap()); } // Combination: Rc<RefCell<T>> for multiple ownership with mutability fn rc_refcell_combination() { use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: Vec<Rc<RefCell<Node>>>, } let root = Rc::new(RefCell::new(Node { value: 5, children: vec![], })); let child = Rc::new(RefCell::new(Node { value: 10, children: vec![], })); root.borrow_mut().children.push(Rc::clone(&child)); // Modify child through root root.borrow().children[0].borrow_mut().value = 15; println!("Child value: {}", child.borrow().value); // 15 } fn main() { box_example(); rc_example(); refcell_example(); arc_example(); rc_refcell_combination(); }

Q13: Explain interior mutability pattern

use std::cell::{RefCell, Cell}; // Cell<T> - for Copy types fn cell_example() { let cell = Cell::new(5); println!("Cell value: {}", cell.get()); cell.set(10); println!("Cell new value: {}", cell.get()); // Update using function cell.update(|x| x + 5); println!("Cell updated: {}", cell.get()); // 15 // Can have multiple references to same Cell let ref1 = &cell; let ref2 = &cell; ref1.set(20); println!("After ref1 set: {}", ref2.get()); // 20 } // RefCell<T> - for non-Copy types fn refcell_deep_dive() { let refcell = RefCell::new(vec![1, 2, 3]); // Immutable borrow { let borrowed = refcell.borrow(); println!("Borrowed: {:?}", *borrowed); } // borrow ends // Mutable borrow { let mut borrowed_mut = refcell.borrow_mut(); borrowed_mut.push(4); borrowed_mut.push(5); } println!("After mutable borrow: {:?}", refcell.borrow()); // Runtime checking let mut_ref = refcell.borrow_mut(); // let immut_ref = refcell.borrow(); // This would panic at runtime drop(mut_ref); // Explicitly drop mutable borrow // Now immutable borrow works let immut_ref = refcell.borrow(); println!("Final: {:?}", *immut_ref); } // Real-world example: Mock objects for testing #[derive(Debug)] struct MockMessenger { messages: RefCell<Vec<String>>, } impl MockMessenger { fn new() -> Self { MockMessenger { messages: RefCell::new(vec![]), } } fn send(&self, message: &str) { // Even though &self is immutable, we can modify messages self.messages.borrow_mut().push(message.to_string()); } fn message_count(&self) -> usize { self.messages.borrow().len() } } // Example with multiple RefCell borrows fn multiple_borrows() -> Result<(), String> { let data = RefCell::new(vec![1, 2, 3]); // try_borrow and try_borrow_mut for non-panicking versions let borrow1 = data.try_borrow(); let borrow2 = data.try_borrow(); match (borrow1, borrow2) { (Ok(b1), Ok(b2)) => { println!("Got two borrows: {:?} and {:?}", *b1, *b2); Ok(()) } _ => Err("Failed to borrow".to_string()), } } // Combining with Rc for shared ownership use std::rc::Rc; fn shared_mutable_data() { let shared = Rc::new(RefCell::new(5)); let shared1 = Rc::clone(&shared); let shared2 = Rc::clone(&shared); *shared1.borrow_mut() += 10; *shared2.borrow_mut() += 20; println!("Final value: {}", shared.borrow()); // 35 } fn main() { cell_example(); refcell_deep_dive(); let messenger = MockMessenger::new(); messenger.send("Hello"); messenger.send("World"); println!("Message count: {}", messenger.message_count()); let _ = multiple_borrows(); shared_mutable_data(); }

7. Memory Management

Q14: Explain Rust's memory safety without garbage collection

// Ownership system prevents memory leaks and dangling pointers fn ownership_memory_safety() { // Stack allocation - automatically cleaned up when function returns let x = 5; let y = 10; // Heap allocation - ownership ensures cleanup let s1 = String::from("hello"); let s2 = s1; // s1 moved, s2 now owns the memory // s1 is no longer valid - prevents use-after-free // println!("{}", s1); // Compile error! // When s2 goes out of scope, memory is automatically freed } // s2 dropped here, memory freed // RAII (Resource Acquisition Is Initialization) fn raii_example() { struct Resource { name: String, } impl Resource { fn new(name: &str) -> Self { println!("Acquiring resource: {}", name); Resource { name: name.to_string(), } } } impl Drop for Resource { fn drop(&mut self) { println!("Releasing resource: {}", self.name); } } let res1 = Resource::new("file.txt"); let res2 = Resource::new("network connection"); // Resources released in reverse order when they go out of scope // "Releasing resource: network connection" // "Releasing resource: file.txt" } // Borrow checker prevents dangling references fn borrow_checker_example() { let r; { let x = 5; // r = &x; // ERROR: x does not live long enough } // x dropped here // println!("{}", r); // r would be dangling } // No memory leaks (except with specific patterns) fn no_memory_leaks() { use std::rc::Rc; use std::cell::RefCell; // Even reference cycles can be handled with Weak use std::rc::Weak; struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); // Weak references don't prevent deallocation println!("Leaf strong: {}, weak: {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf)); } fn main() { ownership_memory_safety(); raii_example(); // borrow_checker_example(); no_memory_leaks(); }

8. Advanced Pattern Matching

Q15: Explain advanced pattern matching techniques

// Destructuring enums enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } // Pattern matching with guards fn match_guards() { 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"), } } // @ bindings fn at_bindings() { let value = Some(5); match value { Some(x @ 1..=5) => println!("Small number: {}", x), Some(x @ 6..=10) => println!("Medium number: {}", x), Some(x) => println!("Large number: {}", x), None => println!("None"), } struct Person { name: String, age: u32, } let person = Person { name: String::from("Alice"), age: 30, }; match person { Person { name: n @ _, age: 30 } => println!("Age 30 person"), Person { name: ref n, age } => println!("Person: {}", n), } } // Matching on references fn match_references() { let x = 5; match &x { &val => println!("Got value: {}", val), } // Using ref keyword let maybe_name = Some(String::from("Alice")); match maybe_name { Some(ref name) => println!("Name: {}", name), None => (), } // Now maybe_name is still usable println!("{:?}", maybe_name); // ref mut for mutable references let mut maybe_num = Some(5); match maybe_num { Some(ref mut num) => *num += 1, None => (), } println!("{:?}", maybe_num); // Some(6) } // Matching on slices and arrays fn match_slices() { let arr = [1, 2, 3, 4, 5]; match arr { [1, second, third, ..] => println!("Starts with 1: {}, {}", second, third), [first, second, .., last] if first < last => println!("Increasing ends"), [.., last] => println!("Last element: {}", last), } let slice: &[i32] = &[1, 2, 3, 4, 5]; match slice { [first, second, tail @ ..] => { println!("First: {}, Second: {}, Tail: {:?}", first, second, tail); } [] => println!("Empty"), } } // Matching on structs fn match_structs() { #[derive(Debug)] struct Point { x: i32, y: i32, } let point = Point { x: 10, y: 20 }; match point { Point { x, y } => println!("Coordinates: ({}, {})", x, y), } match point { Point { x: x_val @ 0..=10, y: 20 } => { println!("x is between 0 and 10: {}", x_val); } Point { x, y } => println!("Other: ({}, {})", x, y), } // Ignoring fields match point { Point { x, .. } => println!("x is {}", x), } } // Matching on multiple patterns fn multiple_patterns() { let x = 1; match x { 1 | 2 => println!("One or two"), 3..=5 => println!("Three to five"), _ => println!("Something else"), } struct Color { r: u8, g: u8, b: u8, } let color = Color { r: 255, g: 0, b: 0 }; match color { Color { r: 255, g: 0, b: 0 } | Color { r: 0, g: 255, b: 0 } => { println!("Primary color"); } _ => println!("Other color"), } } fn main() { match_guards(); at_bindings(); match_references(); match_slices(); match_structs(); multiple_patterns(); }

9. Unsafe Rust

Q16: When and why would you use unsafe Rust?

// Unsafe superpowers: // 1. Dereference raw pointers // 2. Call unsafe functions // 3. Implement unsafe traits // 4. Access/modify mutable statics // 5. Access fields of unions // Raw pointer dereferencing fn raw_pointers() { let mut num = 5; // Create raw pointers (safe) let r1 = &num as *const i32; let r2 = &mut num as *mut i32; // Dereferencing raw pointers (unsafe) unsafe { println!("r1 is: {}", *r1); *r2 = 10; println!("r2 is: {}", *r2); } println!("num is: {}", num); // 10 } // Calling unsafe functions unsafe fn dangerous() { println!("This is dangerous!"); } // FFI (Foreign Function Interface) extern "C" { fn abs(input: i32) -> i32; fn sqrt(input: f64) -> f64; } fn ffi_example() { unsafe { println!("Absolute of -3: {}", abs(-3)); println!("Square root of 2: {}", sqrt(2.0)); } } // Mutable static variables static mut COUNTER: u32 = 0; fn mutable_static() { unsafe { COUNTER += 1; println!("Counter: {}", COUNTER); } } // Implementing unsafe traits unsafe trait UnsafeTrait { fn unsafe_method(&self); } struct MyType; unsafe impl UnsafeTrait for MyType { fn unsafe_method(&self) { println!("Implementing unsafe trait"); } } // Building safe abstractions on unsafe code mod split_at { pub fn split_at_mut<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) { let len = slice.len(); let ptr = slice.as_mut_ptr(); assert!(mid <= len); // This is unsafe but we've validated the indices unsafe { ( std::slice::from_raw_parts_mut(ptr, mid), std::slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) } } } // Custom allocator example use std::alloc::{Layout, alloc, dealloc}; fn custom_allocation() { unsafe { let layout = Layout::new::<i32>(); let ptr = alloc(layout) as *mut i32; if !ptr.is_null() { *ptr = 42; println!("Allocated value: {}", *ptr); dealloc(ptr as *mut u8, layout); } } } // When to use unsafe: // 1. Performance optimizations (SIMD, etc.) // 2. FFI with other languages // 3. Implementing low-level abstractions // 4. Working with hardware // 5. Custom allocators fn main() { raw_pointers(); unsafe { dangerous(); } ffi_example(); mutable_static(); custom_allocation(); let mut data = [1, 2, 3, 4, 5]; let (left, right) = split_at::split_at_mut(&mut data, 3); println!("Left: {:?}, Right: {:?}", left, right); }

10. Performance Optimization

Q17: What optimization techniques do you know in Rust?

// 1. Zero-cost abstractions fn zero_cost() { // Iterator chain compiles to efficient loop let sum: i32 = (0..1000) .filter(|&x| x % 2 == 0) .map(|x| x * x) .sum(); println!("Sum: {}", sum); } // 2. Avoiding unnecessary allocations fn avoid_allocations() { // Bad: creates String then &str let s = "hello world".to_string(); process_str(&s); // Good: use &str directly process_str("hello world"); // Bad: collects into Vec then iterates let numbers: Vec<i32> = (0..1000).collect(); for &n in &numbers { let _ = n * 2; } // Good: iterate without collecting for n in 0..1000 { let _ = n * 2; } } fn process_str(s: &str) { println!("{}", s); } // 3. Using const and compile-time evaluation const TABLE: [i32; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const fn factorial(n: u64) -> u64 { let mut result = 1; let mut i = 1; while i <= n { result *= i; i += 1; } result } const FACT_10: u64 = factorial(10); // 4. Inline hints #[inline(always)] fn small_function(x: i32) -> i32 { x * 2 } #[inline(never)] fn large_function(x: i32) -> i32 { // Complex logic that shouldn't be inlined x * x + x - 1 } // 5. Cache optimization fn cache_optimization() { // Bad: column-major access (cache misses) let matrix = vec![vec![0; 1024]; 1024]; let start = std::time::Instant::now(); for j in 0..1024 { for i in 0..1024 { let _ = matrix[i][j]; } } println!("Column-major: {:?}", start.elapsed()); // Good: row-major access (cache friendly) let start = std::time::Instant::now(); for i in 0..1024 { for j in 0..1024 { let _ = matrix[i][j]; } } println!("Row-major: {:?}", start.elapsed()); } // 6. Memory layout optimization #[repr(C)] // C layout (no reordering) struct Unoptimized { a: u8, b: u64, c: u16, } // Size: 16 bytes (with padding) #[repr(C)] struct Optimized { b: u64, c: u16, a: u8, } // Size: 16 bytes (same but better alignment) // But Rust can reorder fields to minimize padding #[repr(C)] struct Manual { a: u8, // 1 byte // padding 1 byte c: u16, // 2 bytes // padding 4 bytes b: u64, // 8 bytes } // Total: 16 bytes // 7. Using references instead of cloning fn avoid_cloning() { #[derive(Clone)] struct LargeData { data: [u8; 1024], } let data = LargeData { data: [0; 1024] }; // Bad: clones the whole struct fn process_clone(data: LargeData) { println!("First byte: {}", data.data[0]); } // process_clone(data.clone()); // Expensive // Good: borrow instead fn process_borrow(data: &LargeData) { println!("First byte: {}", data.data[0]); } process_borrow(&data); // Cheap // data still usable println!("Still have data: {}", data.data[0]); } // 8. Using SmallVec for small arrays use smallvec::{SmallVec, smallvec}; fn smallvec_example() { // Allocates on stack for small sizes, heap for large let mut vec: SmallVec<[i32; 4]> = smallvec![1, 2, 3]; vec.push(4); // Still on stack vec.push(5); // Now on heap } // 9. Lazy evaluation use lazy_static::lazy_static; lazy_static! { static ref EXPENSIVE_DATA: Vec<i32> = { println!("Computing expensive data..."); (0..1000).collect() }; } // 10. Using Cow (Clone on Write) use std::borrow::Cow; fn cow_example(input: &str) -> Cow<str> { if input.contains(' ') { // Need to modify, so allocate let mut s = input.to_string(); s.push('!'); Cow::Owned(s) } else { // No modification needed, reuse input Cow::Borrowed(input) } } fn main() { zero_cost(); avoid_allocations(); println!("Factorial 10: {}", FACT_10); println!("Double 5: {}", small_function(5)); // cache_optimization(); // Too slow for example avoid_cloning(); let s = cow_example("hello world"); println!("{}", s); }

11. Macros

Q18: Explain declarative and procedural macros

// Declarative macros (macro_rules!) macro_rules! vec2 { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; } macro_rules! calculate { (eval $e:expr) => { { let val: usize = $e; println!("{} = {}", stringify!($e), val); val } }; } macro_rules! hashmap { ( $($key:expr => $value:expr),* $(,)? ) => { { let mut map = std::collections::HashMap::new(); $( map.insert($key, $value); )* map } }; } // Macro with repetition macro_rules! repeat { ($x:expr; $count:expr) => { (0..$count).map(|_| $x).collect::<Vec<_>>() }; } // Macro with multiple arms macro_rules! test_eq { ($left:expr, $right:expr) => { assert_eq!($left, $right); }; ($left:expr, $right:expr, $msg:expr) => { assert_eq!($left, $right, "{}", $msg); }; } // Procedural macros (derive, attribute, function-like) // These are defined in separate crates with proc-macro = true // Example derive macro (would be in separate crate) // use my_derive::MyTrait; // #[derive(MyTrait)] // struct MyStruct { // field: i32, // } // Example attribute macro // #[route(GET, "/")] // fn index() {} // Example function-like macro // let sql = sql!(SELECT * FROM users WHERE id = 1); // Custom derive implementation (simplified) // In real code, this would be in a separate proc-macro crate #[cfg(feature = "proc_macro")] mod proc_macro_example { use proc_macro::TokenStream; #[proc_macro_derive(AnswerFn)] pub fn derive_answer_fn(_item: TokenStream) -> TokenStream { "fn answer() -> u32 { 42 }".parse().unwrap() } } // Using macros fn main() { // Using custom vec macro let v = vec2![1, 2, 3]; println!("{:?}", v); // Calculate macro calculate!(eval 1 + 2 + 3); // Hashmap macro let map = hashmap! { "a" => 1, "b" => 2, "c" => 3, }; println!("{:?}", map); // Repeat macro let repeated: Vec<i32> = repeat!(42; 5); println!("{:?}", repeated); // Test macro with different arms test_eq!(2 + 2, 4); test_eq!(2 + 2, 4, "math is broken"); // Built-in macros println!("file: {}, line: {}", file!(), line!()); // stringify let x = 42; println!("{}", stringify!(x + y)); // concat let s = concat!("Hello", " ", "World", "!"); println!("{}", s); }

12. Async Programming

Q19: Explain async/await in Rust

use futures::executor::block_on; use futures::join; use std::thread; use std::time::Duration; // Basic async function async fn hello_world() { println!("Hello, world!"); } // Async function with await async fn say_hello() { println!("Before await"); hello_world().await; println!("After await"); } // Async function with blocking operation async fn async_sleep(duration: Duration) { // Don't do this - blocks the thread! // thread::sleep(duration); // Use async sleep instead tokio::time::sleep(duration).await; } // Async function that returns a value async fn compute() -> i32 { async_sleep(Duration::from_millis(100)).await; 42 } // Multiple async calls with join async fn process_many() -> (i32, i32, i32) { let (a, b, c) = join!( compute(), compute(), compute(), ); (a, b, c) } // Async streams use futures::stream::{self, StreamExt}; async fn stream_example() { let stream = stream::iter(0..10) .map(|x| x * 2) .filter(|&x| async move { x % 3 == 0 }); tokio::pin!(stream); while let Some(value) = stream.next().await { println!("Got: {}", value); } } // Async with error handling async fn fallible_operation() -> Result<i32, &'static str> { Ok(42) } async fn async_with_error() -> Result<(), &'static str> { let result = fallible_operation().await?; println!("Got: {}", result); Ok(()) } // Using Tokio runtime #[tokio::main] async fn tokio_example() -> Result<(), Box<dyn std::error::Error>> { // Spawn async tasks let handle1 = tokio::spawn(async { compute().await }); let handle2 = tokio::spawn(async { compute().await }); let (result1, result2) = tokio::join!(handle1, handle2); println!("Results: {:?}, {:?}", result1?, result2?); // Timeout use tokio::time::timeout; let result = timeout(Duration::from_millis(10), async_sleep(Duration::from_millis(100))).await; match result { Ok(_) => println!("Completed"), Err(_) => println!("Timeout"), } Ok(()) } // Manual executor (for understanding) fn block_on_example() { let future = compute(); let result = block_on(future); println!("Result: {}", result); } // Async traits (requires async-trait crate) #[async_trait::async_trait] trait AsyncTrait { async fn method(&self) -> i32; } struct MyStruct; #[async_trait::async_trait] impl AsyncTrait for MyStruct { async fn method(&self) -> i32 { 42 } } fn main() { // Basic async block_on(say_hello()); // Multiple futures let results = block_on(process_many()); println!("Results: {:?}", results); // Stream example (would need runtime) // block_on(stream_example()); // Manual executor block_on_example(); // Tokio example - uncomment to run // tokio::runtime::Runtime::new().unwrap().block_on(tokio_example()).unwrap(); }

13. Common Interview Problems

Q20: Implement a thread-safe cache

use std::collections::HashMap; use std::hash::Hash; use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, Instant}; // Simple thread-safe cache struct Cache<K, V> { map: Mutex<HashMap<K, V>>, } impl<K: Eq + Hash + Clone, V: Clone> Cache<K, V> { fn new() -> Self { Cache { map: Mutex::new(HashMap::new()), } } fn get(&self, key: &K) -> Option<V> { let map = self.map.lock().unwrap(); map.get(key).cloned() } fn insert(&self, key: K, value: V) { let mut map = self.map.lock().unwrap(); map.insert(key, value); } fn remove(&self, key: &K) -> Option<V> { let mut map = self.map.lock().unwrap(); map.remove(key) } } // Cache with expiration struct ExpiringCache<K, V> { map: RwLock<HashMap<K, (V, Instant)>>, ttl: Duration, } impl<K: Eq + Hash + Clone, V: Clone> ExpiringCache<K, V> { fn new(ttl: Duration) -> Self { ExpiringCache { map: RwLock::new(HashMap::new()), ttl, } } fn get(&self, key: &K) -> Option<V> { let map = self.map.read().unwrap(); if let Some((value, expires_at)) = map.get(key) { if expires_at > &Instant::now() { Some(value.clone()) } else { None // Expired } } else { None } } fn insert(&self, key: K, value: V) { let mut map = self.map.write().unwrap(); map.insert(key, (value, Instant::now() + self.ttl)); } fn cleanup(&self) { let mut map = self.map.write().unwrap(); let now = Instant::now(); map.retain(|_, (_, expires_at)| expires_at > &now); } } // LRU Cache implementation use std::collections::VecDeque; struct LruCache<K, V> { map: HashMap<K, (V, usize)>, order: VecDeque<K>, capacity: usize, } impl<K: Eq + Hash + Clone, V> LruCache<K, V> { fn new(capacity: usize) -> Self { LruCache { map: HashMap::with_capacity(capacity), order: VecDeque::with_capacity(capacity), capacity, } } fn get(&mut self, key: &K) -> Option<&V> { if let Some((value, pos)) = self.map.get_mut(key) { // Move to front (most recently used) let key_clone = key.clone(); self.order.remove(*pos); self.order.push_front(key_clone); *pos = 0; Some(value) } else { None } } fn insert(&mut self, key: K, value: V) { if self.map.len() >= self.capacity { // Remove least recently used if let Some(lru_key) = self.order.pop_back() { self.map.remove(&lru_key); } } self.order.push_front(key.clone()); self.map.insert(key, (value, 0)); } } // Concurrent access example fn cache_example() { let cache = Arc::new(Cache::<String, i32>::new()); let mut handles = vec![]; for i in 0..10 { let cache = Arc::clone(&cache); handles.push(std::thread::spawn(move || { cache.insert(format!("key_{}", i), i); })); } for handle in handles { handle.join().unwrap(); } for i in 0..10 { if let Some(value) = cache.get(&format!("key_{}", i)) { println!("key_{} = {}", i, value); } } } fn main() { cache_example(); // Expiring cache let expiring = ExpiringCache::<String, i32>::new(Duration::from_secs(2)); expiring.insert("test".to_string(), 42); println!("Got: {:?}", expiring.get(&"test".to_string())); std::thread::sleep(Duration::from_secs(3)); println!("After expiration: {:?}", expiring.get(&"test".to_string())); // LRU Cache let mut lru = LruCache::new(3); lru.insert("a".to_string(), 1); lru.insert("b".to_string(), 2); lru.insert("c".to_string(), 3); lru.get(&"a".to_string()); // a becomes most recent lru.insert("d".to_string(), 4); // Should evict "b" println!("LRU contains a: {}", lru.get(&"a".to_string()).is_some()); println!("LRU contains b: {}", lru.get(&"b".to_string()).is_some()); }

Q21: Implement a custom iterator

// Fibonacci iterator struct Fibonacci { current: u64, next: u64, } impl Fibonacci { fn new() -> Self { Fibonacci { current: 0, next: 1 } } } impl Iterator for Fibonacci { type Item = u64; fn next(&mut self) -> Option<Self::Item> { let new_next = self.current + self.next; let current = self.current; self.current = self.next; self.next = new_next; Some(current) } } // Range iterator with step struct StepRange { start: i32, end: i32, step: i32, current: i32, } impl StepRange { fn new(start: i32, end: i32, step: i32) -> Self { StepRange { start, end, step, current: start, } } } impl Iterator for StepRange { type Item = i32; fn next(&mut self) -> Option<Self::Item> { if self.current >= self.end { None } else { let value = self.current; self.current += self.step; Some(value) } } } // Custom collection with iterator struct MyCollection<T> { data: Vec<T>, } impl<T> MyCollection<T> { fn new() -> Self { MyCollection { data: Vec::new() } } fn push(&mut self, item: T) { self.data.push(item); } } // IntoIterator for MyCollection impl<T> IntoIterator for MyCollection<T> { type Item = T; type IntoIter = std::vec::IntoIter<T>; fn into_iter(self) -> Self::IntoIter { self.data.into_iter() } } // Iter (by reference) impl<'a, T> IntoIterator for &'a MyCollection<T> { type Item = &'a T; type IntoIter = std::slice::Iter<'a, T>; fn into_iter(self) -> Self::IntoIter { self.data.iter() } } // IterMut (by mutable reference) impl<'a, T> IntoIterator for &'a mut MyCollection<T> { type Item = &'a mut T; type IntoIter = std::slice::IterMut<'a, T>; fn into_iter(self) -> Self::IntoIter { self.data.iter_mut() } } // Double-ended iterator struct DoubleRange { start: i32, end: i32, } impl Iterator for DoubleRange { type Item = i32; fn next(&mut self) -> Option<Self::Item> { if self.start < self.end { let value = self.start; self.start += 1; Some(value) } else { None } } } impl DoubleEndedIterator for DoubleRange { fn next_back(&mut self) -> Option<Self::Item> { if self.start < self.end { self.end -= 1; Some(self.end) } else { None } } } fn main() { // Fibonacci let fib = Fibonacci::new(); println!("First 10 Fibonacci numbers:"); for (i, num) in fib.take(10).enumerate() { println!("F{} = {}", i, num); } // Step range let step_range = StepRange::new(0, 10, 2); println!("Step range (0 to 10 step 2): {:?}", step_range.collect::<Vec<_>>()); // Custom collection let mut collection = MyCollection::new(); collection.push(1); collection.push(2); collection.push(3); println!("Custom collection (owned):"); for item in collection.into_iter() { println!("{}", item); } // collection is moved // By reference let mut collection = MyCollection::new(); collection.push(1); collection.push(2); collection.push(3); println!("Custom collection (reference):"); for item in &collection { println!("{}", item); } println!("Still have collection: {:?}", collection.data); // Double-ended let double = DoubleRange { start: 0, end: 5 }; println!("Double-ended iterator (forward): {:?}", double.clone().collect::<Vec<_>>()); println!("Double-ended iterator (backward): {:?}", double.rev().collect::<Vec<_>>()); // Iterator combinators let sum: i32 = StepRange::new(1, 11, 1) .filter(|&x| x % 2 == 0) .map(|x| x * x) .sum(); println!("Sum of squares of evens from 1-10: {}", sum); }

Q22: Implement a custom smart pointer

use std::ops::{Deref, DerefMut}; use std::fmt; // Simple smart pointer struct MyBox<T> { value: T, } impl<T> MyBox<T> { fn new(value: T) -> Self { MyBox { value } } fn into_inner(self) -> T { self.value } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.value } } impl<T> DerefMut for MyBox<T> { fn deref_mut(&mut self) -> &mut T { &mut self.value } } impl<T: fmt::Display> fmt::Display for MyBox<T> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "MyBox({})", self.value) } } // Reference counted smart pointer (simplified) struct MyRc<T> { value: *mut T, ref_count: *mut usize, } impl<T> MyRc<T> { fn new(value: T) -> Self { let value = Box::into_raw(Box::new(value)); let ref_count = Box::into_raw(Box::new(1)); MyRc { value, ref_count, } } fn clone(&self) -> Self { unsafe { *self.ref_count += 1; } MyRc { value: self.value, ref_count: self.ref_count, } } } impl<T> Deref for MyRc<T> { type Target = T; fn deref(&self) -> &T { unsafe { &*self.value } } } impl<T> Drop for MyRc<T> { fn drop(&mut self) { unsafe { *self.ref_count -= 1; if *self.ref_count == 0 { drop(Box::from_raw(self.value)); drop(Box::from_raw(self.ref_count)); println!("Last reference dropped, cleaning up"); } } } } // Clone on write smart pointer enum CowData<'a, T> { Borrowed(&'a T), Owned(T), } struct MyCow<'a, T> { data: CowData<'a, T>, } impl<'a, T: Clone> MyCow<'a, T> { fn new_borrowed(value: &'a T) -> Self { MyCow { data: CowData::Borrowed(value), } } fn new_owned(value: T) -> Self { MyCow { data: CowData::Owned(value), } } fn to_mut(&mut self) -> &mut T { match self.data { CowData::Borrowed(value) => { // Clone the borrowed data let owned = value.clone(); self.data = CowData::Owned(owned); match &mut self.data { CowData::Owned(ref mut v) => v, _ => unreachable!(), } } CowData::Owned(ref mut v) => v, } } } impl<'a, T> Deref for MyCow<'a, T> { type Target = T; fn deref(&self) -> &T { match &self.data { CowData::Borrowed(v) => v, CowData::Owned(v) => v, } } } // Usage examples fn main() { // MyBox let mut boxed = MyBox::new(42); println!("Boxed: {}", *boxed); *boxed = 100; println!("Modified: {}", boxed); // Auto-deref fn takes_i32(x: &i32) { println!("Got: {}", x); } takes_i32(&boxed); // Methods work through deref let boxed_string = MyBox::new(String::from("hello")); println!("Length: {}", boxed_string.len()); // Deref to String // MyRc let rc1 = MyRc::new(5); { let rc2 = rc1.clone(); let rc3 = rc1.clone(); println!("rc1: {}, rc2: {}, rc3: {}", *rc1, *rc2, *rc3); } // rc2 and rc3 drop here println!("rc1 still valid: {}", *rc1); // rc1 drops here // MyCow let data = 42; let mut cow = MyCow::new_borrowed(&data); println!("Borrowed: {}", *cow); // Now we need to modify *cow.to_mut() += 10; println!("After modification: {}", *cow); }

Interview Tips

Key Topics to Review

  1. Ownership and Borrowing: Understand move semantics, borrowing rules, and common patterns
  2. Lifetimes: Be able to explain and write lifetime annotations
  3. Traits: Know common traits (Debug, Clone, Copy, PartialEq, etc.) and when to implement them
  4. Error Handling: Understand Result, Option, ? operator, and custom error types
  5. Concurrency: Know threads, channels, Arc, Mutex, and Send/Sync
  6. Smart Pointers: Box, Rc, RefCell, Arc and their use cases
  7. Memory Management: RAII, Drop trait, no GC
  8. Pattern Matching: Advanced patterns, destructuring, guards
  9. Generics and Traits: Monomorphization, trait objects, associated types
  10. Unsafe Rust: When and why to use it

Common Interview Questions

  1. "Explain ownership and borrowing in Rust"
  2. "What's the difference between String and &str?"
  3. "Explain lifetimes with examples"
  4. "How does Rust ensure memory safety without GC?"
  5. "Compare Box, Rc, and Arc"
  6. "Explain the difference between trait objects and generics"
  7. "How would you handle errors in Rust?"
  8. "Explain Send and Sync traits"
  9. "What's the difference between panic! and Result?"
  10. "How would you implement a thread-safe cache?"

Code to Practice

  1. Implement a thread-safe counter
  2. Write a custom iterator
  3. Create a simple web server
  4. Parse JSON with serde
  5. Implement a concurrent data structure
  6. Write a macro
  7. Use async/await for I/O operations
  8. Implement a custom smart pointer
  9. Create a type-safe state machine
  10. Write a generic collection

Remember to:

  • Think out loud during problem-solving
  • Explain your reasoning before writing code
  • Discuss trade-offs of different approaches
  • Show Rust idioms rather than writing C-style code
  • Mention edge cases and error handling
  • Ask clarifying questions about requirements

Leave a Reply

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


Macro Nepal Helper