Table of Contents
- Ownership and Borrowing Deep Dive
- Lifetimes
- Traits and Generics
- Error Handling
- Concurrency
- Smart Pointers
- Memory Management
- Advanced Pattern Matching
- Unsafe Rust
- Performance Optimization
- Macros
- Async Programming
- 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
- Ownership and Borrowing: Understand move semantics, borrowing rules, and common patterns
- Lifetimes: Be able to explain and write lifetime annotations
- Traits: Know common traits (Debug, Clone, Copy, PartialEq, etc.) and when to implement them
- Error Handling: Understand Result, Option, ? operator, and custom error types
- Concurrency: Know threads, channels, Arc, Mutex, and Send/Sync
- Smart Pointers: Box, Rc, RefCell, Arc and their use cases
- Memory Management: RAII, Drop trait, no GC
- Pattern Matching: Advanced patterns, destructuring, guards
- Generics and Traits: Monomorphization, trait objects, associated types
- Unsafe Rust: When and why to use it
Common Interview Questions
- "Explain ownership and borrowing in Rust"
- "What's the difference between String and &str?"
- "Explain lifetimes with examples"
- "How does Rust ensure memory safety without GC?"
- "Compare Box, Rc, and Arc"
- "Explain the difference between trait objects and generics"
- "How would you handle errors in Rust?"
- "Explain Send and Sync traits"
- "What's the difference between panic! and Result?"
- "How would you implement a thread-safe cache?"
Code to Practice
- Implement a thread-safe counter
- Write a custom iterator
- Create a simple web server
- Parse JSON with serde
- Implement a concurrent data structure
- Write a macro
- Use async/await for I/O operations
- Implement a custom smart pointer
- Create a type-safe state machine
- 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