Rust
30 questions | entry to principal | answers written to be said out loud, not read
entry
What is ownership in Rust?
Ownership is Rust's rule that every value has exactly one owner at a time, and when that owner goes out of scope the value is dropped. It replaces manual memory management and garbage collection with compile time checks. When I assign a value to a new variable or pass it into a function, ownership moves unless the type implements Copy.
What happens when you move a value in Rust?
Moving transfers ownership from one binding to another, and the original binding becomes invalid so the compiler will reject any further use of it. This avoids double frees because only one owner is ever responsible for cleaning up the value. For types that are cheap to duplicate, like integers, Rust implements Copy instead so no move happens at all.
What is borrowing, and what does the borrow checker actually enforce?
Borrowing lets me use a value through a reference without taking ownership of it. The borrow checker enforces that I can have either one mutable reference or any number of immutable references to a value at a time, never both together. That rule is what prevents data races and use-after-free bugs at compile time instead of at runtime.
How does Rust think about the stack versus the heap?
Fixed size, known-at-compile-time data like integers and simple structs live on the stack, which is fast to allocate and clean up. Data with a size that can grow, like a String or a Vec, stores its contents on the heap and keeps a pointer, length, and capacity on the stack. Ownership rules apply the same way to both, but heap data is what actually gets freed when the owner goes out of scope.
What are Vec and slices, and how do they relate?
Vec is a growable, heap allocated, owned collection of values of one type. A slice is a borrowed view into a contiguous sequence, like a Vec or an array, described by a pointer and a length. I use slices constantly in function signatures because they let a function accept a Vec, an array, or part of either without caring about ownership.
How does Rust handle the absence of a value and the possibility of failure?
Option represents a value that might or might not be present, with variants Some and None, and it replaces null entirely. Result represents an operation that can succeed or fail, with variants Ok and Err carrying a value or an error. Both are enums the compiler forces me to handle, so I cannot accidentally skip the failure or empty case the way I could with null or an unchecked exception.
How do structs work in Rust?
A struct groups related named fields into one type, similar to a class without inheritance. I attach behavior to a struct through impl blocks rather than defining methods inside the struct itself, and I compose functionality through traits instead of subclassing. Rust also has tuple structs and unit structs for lighter weight cases where named fields aren't necessary.
What is a trait, and how do you think about it?
A trait defines a set of methods a type must implement, which is how Rust does shared behavior instead of inheritance. I implement a trait for a type with an impl block, and I can require a generic function to accept any type that implements a given trait. Traits can also provide default method implementations, so implementers only override what they need to.
What role does cargo play in the Rust ecosystem?
Cargo is Rust's build tool and package manager, it compiles my project, resolves and downloads dependencies called crates from crates.io, runs tests, and manages versioning through Cargo.toml and a locked Cargo.lock file. It also runs formatting and linting through rustfmt and clippy. Having one standard tool for all of that removes a lot of the tooling fragmentation I'd otherwise deal with in other ecosystems.
junior
What are lifetimes and why does the compiler need them?
A lifetime describes how long a reference is guaranteed to stay valid relative to the data it points to. The compiler uses lifetimes to make sure a reference never outlives the value it refers to, which is what prevents dangling references. Most of the time lifetimes are inferred, and I only write them out explicitly when a function signature has multiple references and the compiler cannot tell how they relate.
What is the difference between Copy and Clone?
Copy is an implicit, cheap, bitwise duplication that happens automatically for simple types like integers and booleans, so assigning them doesn't move the original. Clone is explicit and can be arbitrarily expensive, like deep-copying a String or a Vec, so I have to call .clone() on purpose. A type can implement both, but Copy is only allowed on types where a bitwise copy is actually correct.
What is the difference between String and &str?
String is an owned, growable, heap allocated string type that I use when I need to build or store text. &str is a borrowed string slice, a view into string data owned by something else, often used for function parameters since it accepts both a String reference and a string literal. I generally take &str in function signatures and only reach for String when I actually need ownership.
What does the question mark operator do?
The question mark operator unwraps a Result or Option inside a function, and if the value is Err or None it returns early with that error or None from the current function. It saves me from writing a match statement every time I want to propagate a failure upward instead of handling it right there. It only works in a function whose return type is compatible with what it's propagating.
Why is exhaustiveness in Rust's match important?
The compiler requires every match on an enum to cover every variant, or explicitly acknowledge the rest with a catch-all arm. That means if I add a new variant to an enum later, the compiler points me at every place in the codebase that needs to be updated to handle it. It turns a whole category of bugs, forgetting to handle a case, into a compile error instead of a runtime surprise.
What makes Rust enums different from enums in most other languages?
Rust enums are sum types, meaning each variant can carry its own different data, not just a name. That lets me model something like a Shape enum where Circle carries a radius and Rectangle carries a width and height, all under one type. Combined with exhaustive matching, enums are how I model state machines and domain logic instead of reaching for a class hierarchy.
What do derive macros do, and why do you use them?
Derive macros generate boilerplate trait implementations at compile time, so writing #[derive(Debug, Clone, PartialEq)] above a struct gives me formatting, cloning, and equality without writing any of that code by hand. They keep common traits consistent and save me from bugs in hand written boilerplate. Custom derive macros can also be written for project specific traits, which is common in larger Rust codebases.
How do Rust iterators work, and what does laziness mean here?
An iterator produces values one at a time through its next method, and adapters like map and filter are lazy, meaning they don't actually run until something consumes the iterator, like collect or a for loop. That laziness lets the compiler fuse a whole chain of adapters into tight code with no intermediate allocations. It's a big part of why idiomatic, high level Rust can still be as fast as a hand written loop.
What is the difference between Fn, FnMut, and FnOnce?
Fn borrows its captured variables immutably and can be called repeatedly, FnMut borrows them mutably and can also be called repeatedly, and FnOnce takes ownership of its captures and can only be called one time. The compiler infers which trait a closure implements based on how it actually uses what it captures. I mostly only think about this when a function signature demands a specific one, like FnOnce for something that consumes a value once.
What are Box, Rc, and Arc for?
Box puts a value on the heap with a single owner, which I use for recursive types or when I need a known size for something like a trait object. Rc adds shared ownership through reference counting, letting multiple owners share the same heap value, but it's only safe within a single thread. Arc is the same idea as Rc except the reference count is updated atomically, which makes it safe to share across threads.
senior
Why doesn't Rust have a garbage collector?
Rust gets memory safety from ownership and borrowing checked at compile time, so it does not need a runtime garbage collector to track and free memory. That means there is no unpredictable pause for collection and no runtime overhead for tracking references, which matters for systems programming, embedded targets, and latency sensitive services. The tradeoff is that I have to think about ownership up front instead of letting a collector clean up later.
Trait objects versus generics, when do you reach for each?
Generics are resolved at compile time through monomorphization, so each concrete type gets its own generated code with no runtime cost, but every type used has to be known at compile time. Trait objects, written as dyn Trait, use a vtable and dynamic dispatch at runtime, which lets me store different concrete types behind one interface, like a Vec of dyn Shape. I default to generics for performance critical code and reach for trait objects when I genuinely need runtime polymorphism, like a plugin list.
What is impl Trait used for?
impl Trait lets me say a function returns some type that implements a trait without naming the concrete type, which is essential for returning closures or iterator chains whose real type would be unwieldy or unnameable. In argument position it's shorthand for a generic bound. Unlike a trait object, it's still statically resolved, so there's no dynamic dispatch cost.
How do you decide between panicking and returning a Result?
I return a Result for anything that's an expected, recoverable failure, like a missing file, invalid input, or a failed network call, so the caller can decide what to do about it. I reach for panic only for programming bugs and truly unrecoverable states, things that should never happen if the code is correct, like an invariant being violated. For libraries I almost always prefer Result, and for custom errors I implement std::error::Error or use a crate like thiserror so failures carry useful context.
What is interior mutability, and what does RefCell give you?
Interior mutability lets me mutate data even through a shared, immutable reference, by moving the borrow checking from compile time to runtime. RefCell is the common way to do this in single threaded code, it tracks borrows at runtime and panics if I violate the same one-mutable-or-many-immutable rule the compiler would normally enforce. I use it when the type system's static rules are too strict for a pattern I know is actually safe, like a shared cache.
What does zero-cost abstraction mean in Rust?
It means a high level feature, like an iterator chain, generics, or a trait bound, compiles down to code that's just as fast as the equivalent hand written low level version, with no extra runtime cost for using the abstraction. Generics are monomorphized into specialized code per type instead of relying on runtime dispatch, and iterator adapters get inlined and fused by the compiler. It's the reason I can write expressive, high level Rust without trading away systems level performance.
staff
What do Send and Sync mean?
Send means a type is safe to move to another thread, and Sync means a type is safe to share by reference across threads. Most types are automatically both, but something like Rc is deliberately not Send or Sync because its reference counting isn't atomic and would cause data races. The compiler enforces these as marker traits, so a whole category of thread safety bugs gets caught before the program ever runs.
How do you share state safely across threads in Rust?
For message passing I use channels, sending owned values from one thread to another so only one thread touches the data at a time. When threads genuinely need to share the same state, I wrap it in a Mutex for exclusive access and usually an Arc around that so multiple threads can hold a reference to the same lock. The type system forces me to call lock before touching the data, so it's very hard to accidentally read shared state without synchronizing.
What does async/await actually do in Rust, and where does tokio fit in?
An async function returns a future, a value representing work that hasn't finished yet, and nothing actually runs until that future is polled by a runtime. Rust doesn't ship a built in async runtime, so I bring in one like tokio to actually schedule and drive futures, handle timers, and do async IO. I reach for async when I'm doing IO bound work with a lot of concurrent waiting, and plain threads when the work is CPU bound.
principal
When is it actually justified to write unsafe code?
unsafe lets me do things the compiler can't verify are safe, like dereferencing a raw pointer, calling into C through FFI, or implementing a low level data structure that needs a shared mutable buffer. I use it only when I have a genuine, well understood reason, and I keep the unsafe block as small as possible and wrap it in a safe public API so callers never have to think about it. Reaching for unsafe just to silence the borrow checker without understanding why it's complaining is a mistake.
When would you not reach for Rust?
If I need to move fast on a prototype or a small script where compile times and the borrow checker's learning curve would slow the team down more than they'd help, I'd reach for something like Python or TypeScript instead. Rust also doesn't have the ecosystem maturity in every domain, so if a project leans heavily on a mature library that only exists in another language, that matters. I think of Rust as the right call when correctness, performance, and memory safety are worth the upfront investment, not as a default for every project.
Fast recall
ownership = one owner at a time | borrow = temporary reference to a value | lifetime = how long a reference is valid | move = ownership transferred, not copied | Copy = implicit bitwise duplicate | Clone = explicit deep duplicate | Option = value or nothing | Result = success or error | trait = shared behavior contract | Box = heap allocation, single owner | Rc = shared ownership, single thread | Arc = shared ownership, thread safe | Mutex = exclusive access lock | unsafe = opt out of some guarantees