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