Idiomatic Rust
Core Interview Questions
1. What does "idiomatic Rust" mean?
Idiomatic Rust means writing code that embraces Rust's core features rather than working around them. It's about leveraging ownership, borrowing, the type system, and Rust's standard library to write safe, efficient, and expressive code. Instead of fighting the borrow checker or reaching for unsafe, you design your code to work naturally with Rust's constraints.
2. How do you identify non-idiomatic Rust code?
Non-idiomatic Rust often looks like code written in another language (C++, Java, Python) that was transliterated into Rust. Common signs: overuse of Rc<RefCell<T>> or unsafe to work around ownership, ignoring Result and Option with .unwrap(), using index-based loops instead of iterators, or reinventing utilities that exist in the standard library.
3. Why is idiomatic Rust important?
Idiomatic Rust is safer, more maintainable, and often more efficient. When you work with the language instead of against it, the compiler helps catch bugs at compile time. Other Rust developers can read and understand your code more easily. And Rust's zero-cost abstractions mean idiomatic code is usually just as fast as hand-rolled alternatives.
4. What's the relationship between idiomatic Rust and performance?
Idiomatic Rust is typically faster than naive implementations. Rust's abstractions (iterators, Option, Result) compile down to the same assembly you'd write by hand, but with safety guarantees. The difference is that idiomatic code expresses intent clearly while letting the optimizer do its job.
Code Snippets
Error Handling: unwrap() vs ? Operator
Non-idiomatic:
use std::fs; fn read_file_bad(path: &str) -> String { let content = fs::read_to_string(path).unwrap(); // Crashes on error content } fn main() { let data = read_file_bad("example.txt"); println!("{}", data); }
Idiomatic:
use std::fs; use std::io; fn read_file(path: &str) -> Result<String, io::Error> { let content = fs::read_to_string(path)?; // Propagates error Ok(content) } fn main() { match read_file("example.txt") { Ok(content) => println!("{}", content), Err(e) => eprintln!("Error reading file: {}", e), } }
Loops: Index-based vs Iterators
Non-idiomatic:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; // C-style loop with indices let mut sum = 0; for i in 0..numbers.len() { sum += numbers[i]; } println!("Sum: {}", sum); }
Idiomatic:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; // Iterator with functional style let sum: i32 = numbers.iter().sum(); println!("Sum: {}", sum); // Or with more complex transformations let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect(); println!("Doubled: {:?}", doubled); }
Option Handling: unwrap() vs Pattern Matching
Non-idiomatic:
fn main() { let maybe_number = Some(42); // Crash if None let number = maybe_number.unwrap(); println!("Number: {}", number); }
Idiomatic:
fn main() { let maybe_number = Some(42); // Handle both cases explicitly match maybe_number { Some(n) => println!("Number: {}", n), None => println!("No number"), } // Or use if-let for one case if let Some(n) = maybe_number { println!("Got number: {}", n); } // Or use combinators let doubled = maybe_number.map(|n| n * 2); println!("Doubled: {:?}", doubled); }
String Handling: &str vs String for Function Arguments
Non-idiomatic:
// Forces caller to allocate, even for static strings fn greet_bad(name: String) { println!("Hello, {}!", name); } fn main() { let name = "Alice".to_string(); // Unnecessary allocation greet_bad(name); }
Idiomatic:
// Accepts both &str and String, no allocation needed fn greet(name: &str) { println!("Hello, {}!", name); } fn main() { let static_str = "Alice"; let owned_string = String::from("Bob"); greet(static_str); // Works with &str greet(&owned_string); // Works with &String }
Ownership: Rc<RefCell<T>> vs Structured Ownership
Non-idiomatic:
use std::rc::Rc; use std::cell::RefCell; struct Node { value: i32, // Using RefCell because ownership wasn't designed properly parent: Option<Rc<RefCell<Node>>>, } impl Node { fn new(value: i32) -> Self { Node { value, parent: None, } } } fn main() { let parent = Rc::new(RefCell::new(Node::new(1))); let child = Rc::new(RefCell::new(Node::new(2))); // Runtime borrow checking, can panic! child.borrow_mut().parent = Some(parent.clone()); println!("Child value: {}", child.borrow().value); }
Idiomatic:
struct Node { value: i32, // Clear ownership structure, no runtime checks needed } struct Tree { root: Node, children: Vec<Node>, } impl Tree { fn new(root_value: i32) -> Self { Tree { root: Node { value: root_value }, children: Vec::new(), } } fn add_child(&mut self, value: i32) { self.children.push(Node { value }); } } fn main() { let mut tree = Tree::new(1); tree.add_child(2); tree.add_child(3); println!("Root: {}", tree.root.value); for child in &tree.children { println!("Child: {}", child.value); } }
Collection Building: push in Loop vs collect from Iterator
Non-idiomatic:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; // Manual loop and mutation let mut evens = Vec::new(); for num in &numbers { if num % 2 == 0 { evens.push(num * 2); } } println!("Evens doubled: {:?}", evens); }
Idiomatic:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; // Declarative style with iterators let evens: Vec<i32> = numbers .iter() .filter(|&&n| n % 2 == 0) .map(|&n| n * 2) .collect(); println!("Evens doubled: {:?}", evens); }
Constructors: new() vs Builder Pattern
Simple case - new() is idiomatic:
struct Point { x: f64, y: f64, } impl Point { fn new(x: f64, y: f64) -> Self { Point { x, y } } } fn main() { let p = Point::new(1.0, 2.0); println!("Point: ({}, {})", p.x, p.y); }
Complex case - Builder pattern is idiomatic:
struct Server { host: String, port: u16, max_connections: usize, timeout_secs: u64, } struct ServerBuilder { host: String, port: u16, max_connections: Option<usize>, timeout_secs: Option<u64>, } impl ServerBuilder { fn new(host: impl Into<String>, port: u16) -> Self { ServerBuilder { host: host.into(), port, max_connections: None, timeout_secs: None, } } fn max_connections(mut self, max: usize) -> Self { self.max_connections = Some(max); self } fn timeout_secs(mut self, secs: u64) -> Self { self.timeout_secs = Some(secs); self } fn build(self) -> Server { Server { host: self.host, port: self.port, max_connections: self.max_connections.unwrap_or(100), timeout_secs: self.timeout_secs.unwrap_or(30), } } } fn main() { let server = ServerBuilder::new("localhost", 8080) .max_connections(500) .timeout_secs(60) .build(); println!("Server: {}:{}", server.host, server.port); }