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