Test Your Rust Knowledge
This quiz covers Rust fundamentals through to advanced concepts. It is divided into two parts: the basics (30 questions) and hardcore mode (10 questions). Answers are hidden below each question -- try to answer them before peeking.
Most intermediate Rust developers get stuck by the 3rd or 4th question in the hardcore section. If you can articulate a clear reasoning on at least half of them, you can consider yourself a confirmed expert.
Part 1: Fundamentals
Language basics
Question 1: What are the main differences between Rust and C/C++?
Show answer
Memory safety -- or more precisely, resource safety through ownership and the borrow checker. In Rust, memory is managed at compile time, without a garbage collector, which guarantees safety without sacrificing performance.
Question 2: Can you explain the ownership system in Rust?
Show answer
Data can only be owned by a single variable. Functions can access it through a reference (optionally mutable, i.e., for writing), but you can only "transfer" data to another variable, which means the previous owner no longer has access. The only alternative is copying (or cloning).
Question 3: What is the difference between &T and &mut T?
Show answer
&Tis a read-only reference to a type T.&mut Tis a read-write reference to a type T.
You can have multiple &T simultaneously, but only one &mut T at a time, and never alongside a &T.
Question 4: What is the borrow checker and what problem does it solve?
Show answer
The borrow checker is the compiler mechanism that ensures borrowing rules are respected. It prevents data races, dangling references, and invalid memory accesses, all verified at compile time.
Question 5: What is a slice in Rust and how does it differ from a Vec<T>?
Show answer
A slice (&[T]) provides access to a portion of an array in Rust, managed by the compiler. Vec<T> is a runtime mechanism (from the std lib) that additionally allows heap memory allocations. A slice is a view on existing data; a Vec owns its data.
Question 6: What is the difference between String and &str?
Show answer
Like slices and Vec, String is a structure that allows dynamic memory allocation on the heap. &str is a reference to a static string or a portion of a String. String is owned; &str is borrowed.
Question 7: What does the move keyword mean?
Show answer
It tells the compiler that variables should be "transferred" into a closure. The closure then takes ownership of the captured variables, which is mandatory for example when sending a closure to another thread.
Question 8: What is the unsafe keyword for?
Show answer
It allows operations typically forbidden by the borrow checker: dereferencing raw pointers, calling unsafe functions, accessing mutable static variables, implementing unsafe traits, and accessing union fields.
Question 9: What is the difference between Box<T>, Rc<T>, and Arc<T>?
Show answer
Box<T>: simple heap allocation.Rc<T>: Box with reference counting (single-thread only).Arc<T>: same but with atomic operations, so it supports multi-threading (implements Send + Sync).
Question 10: How do you implement a trait on a struct?
Show answer
With the impl MyTrait for MyStruct { ... } block. You then define each method required by the trait for that specific struct.
Error handling
Question 11: What is the difference between Result<T, E> and Option<T>?
Show answer
Resultis an enum with two values:Ok(T)andErr(E). It is tied to the?operator, syntactic sugar for simplifying error handling.Optionis an enum with two values:Some(T)andNone. It represents a potentially absent value.
Question 12: How does the ? operator work?
Show answer
It is the equivalent of unwrap() but performs a return Err(e) instead of panicking. If the Result is Ok, the value is extracted. If it is an Err, the function returns immediately with the error.
Question 13: Why is unwrap() dangerous?
Show answer
unwrap() transforms a Result<T, E> into T, but panics if it is an Err. In production, a panic can crash the entire program. Prefer using ?, unwrap_or, unwrap_or_else, or explicit pattern matching.
Generics and traits
Question 14: What is the difference between traits (Rust) and interfaces (other languages)?
Show answer
In Go, for example, an interface uses duck typing: any type that implements the methods implicitly satisfies the interface. In Rust, a trait requires an explicit implementation with impl Trait for Type. Rust traits can also provide default implementations, associated types, and generic constraints.
Question 15: What is the difference between dyn Trait and impl Trait?
Show answer
dyn Traitgenerates a vtable for method access at runtime (dynamic dispatch). There is an indirection cost on each call.impl Traitlets the compiler choose the concrete type at compile time (static dispatch, monomorphization). No runtime cost.
Question 16: What does Sized mean in Rust?
Show answer
It means the type has a size known at compile time. Most types are Sized by default. Dynamic types like str or dyn Trait are not Sized, which requires handling them behind a reference or a Box.
Concurrency and async
Question 17: How does concurrency work in Rust?
Show answer
There are two main approaches to concurrency in Rust:
- System threads directly (
std::thread::spawn). - The
async/awaitkeywords, which are based on the Future trait (comparable to JavaScript Promises).
Send and Sync are traits that guarantee a variable can be sent to or shared between threads. Channels (std::sync::mpsc) provide a communication channel implementation, similar to Go's CSP channels.
Question 18: What is the difference between tokio and async-std?
Show answer
Both are asynchronous runtimes for Rust. Tokio is the most widely used in the ecosystem and offers a full runtime with scheduled coroutine management. async-std aims to be closer to the standard library API. In practice, Tokio dominates the async Rust landscape by a wide margin.
Question 19: What does Pin<Box<dyn Future<Output = T>>> mean?
Show answer
Pinprevents the data from being moved in memory (which the compiler might otherwise do for optimization). This is crucial for futures that contain self-references.Boxallocates the data on the heap.dyn Future<Output = T>indicates that the result will be available when the computing routine completes. There is apollmethod to check, and a notification mechanism (Waker) to avoid active polling.
Question 20: How is future memory managed in Rust without a GC?
Show answer
Through the borrow checker and ownership system. When a variable is no longer needed (goes out of scope), it is dropped automatically, freeing the associated resource. No garbage collector needed.
Memory and low-level
Question 21: What is a lifetime ('a) and why is it useful?
Show answer
It is the validity period of a reference, i.e., the period during which it is valid. It is useful because it allows the compiler to determine when a reference is no longer valid and thus guarantee that freed memory is never accessed.
Question 22: Give a concrete case of complex lifetimes.
Show answer
Typically, when passing a reference to a struct that will embed the reference. It gets more complex in concurrent routines. And the most disorienting case is when you think in C/C++ terms and try to create cyclic references -- which Rust forbids by design.
Question 23: How does Rust guarantee the absence of data races?
Show answer
Through the borrow checker and the borrowing system. A variable accessible through an immutable reference cannot be modified by another routine at the same time. The compiler guarantees that there cannot simultaneously be a mutable reference and immutable references to the same data.
Question 24: When should you use Cow<'a, str>?
Show answer
Cow stands for Copy-On-Write. It avoids unnecessary copies: you keep a reference as long as you do not need to modify the data. If a modification is needed, only then is a copy made. It is useful when a function sometimes returns an existing reference and sometimes needs to create a new String.
Tools and ecosystem
Question 25: What does cargo check do compared to cargo build?
Show answer
cargo check verifies that the code compiles without generating a binary artifact. It is much faster than cargo build and is sufficient for verification during development.
Question 26: What is cargo clippy for?
Show answer
It is a linter for Rust. It detects non-idiomatic code patterns, common mistakes, and suggests improvements. It is a complementary tool to the compiler for writing clean and performant Rust code.
Question 27: What does cargo fmt do?
Show answer
It is the automatic code formatting tool for Rust following standard conventions (rustfmt). It reformats code to guarantee a uniform style throughout the project.
Question 28: What Rust crates do you use most?
Show answer
Commonly used crates in the Rust ecosystem include: Tokio (async runtime), Serde (serialization), Anyhow/thiserror (error handling), Hyper (HTTP), Rayon (parallelism), Wgpu (graphics), Yew (frontend WASM). The choice depends heavily on the application domain.
Advanced design
Question 29: How do you implement a trait object pattern in Rust?
Show answer
You use dyn Trait behind a pointer (Box<dyn Trait>, &dyn Trait, Arc<dyn Trait>). This is useful for example in a plugin system: you define a Plugin trait with the required methods, and each plugin implements it. The system stores a collection of Box<dyn Plugin> and calls methods via dynamic dispatch.
Question 30: How would you approach writing a crate for high-performance computing?
Show answer
- Start by implementing the computation reliably with unit tests.
- Parallelize with Rayon for multi-threading or distribute with appropriate crates.
- Ensure tests pass at every step.
- Implement instrumentation to measure performance.
- Make the computation fast while allowing multiple simultaneous computations.
The most important thing: do not optimize prematurely, and measure before making changes.
Part 2: Hardcore mode
These questions test depth of understanding: memory (ownership, pinning, UB), advanced types (variance, HRTB), low-level (unsafe, atomics, no-std), and ecosystem (FFI, tools like Miri).
Question H1: Unsafe Rust
What guarantees remain valid in unsafe mode, and which ones can you break if you make a mistake? Give an example where unsafe is indispensable but justified.
Show answer
Even in unsafe, Rust still guarantees language-level memory safety if the code is correct. What you can break: mutable aliasing, lifetime respect, dangling pointers. unsafe is a contract: the developer promises the compiler that invariants are maintained manually.
Justified example: writing a wrapper around a C API (extern "C") or directly manipulating memory with std::ptr::copy_nonoverlapping.
Question H2: Pinning and Unpin
Explain the role of the Pin<T> type in async Rust. Why can some types (like Future) not be moved in memory after they have started executing?
Show answer
Pin<T> prevents moving an object in memory after its initialization. This is critical for Futures: they store internal pointers to themselves (self-references), so moving them would invalidate those pointers.
By default, types are Unpin, meaning they can be moved freely. Only certain types (like futures containing self-references, marked !Unpin) require pinning.
Question H3: Trait coherence and Orphan rule
Describe the coherence rule and the orphan rule in Rust. Why are they necessary?
Show answer
The coherence rule guarantees that a trait implementation is unique for a given type across the entire program. The orphan rule adds a restriction: you cannot implement a foreign trait for a foreign type -- at least one of the two must belong to you.
This prevents conflicts during trait resolution (the "diamond problem") and guarantees that compilation remains coherent and deterministic.
Workaround: the newtype pattern. Create struct MyType(ForeignType) and implement the foreign trait on your newtype.
struct MyString(String); impl std::fmt::Display for MyString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Custom: {}", self.0) } }
Question H4: HRTB (Higher-Rank Trait Bounds)
Explain what a constraint like for<'a> Fn(&'a T) -> &'a U means. Give a concrete case where this kind of constraint is necessary.
Show answer
for<'a> Fn(&'a T) -> &'a U means: the closure must work for all lifetimes 'a. It is not tied to a specific lifetime, but to any arbitrary one.
This is useful in generic APIs like iterators (Fn(&T) -> bool regardless of the lifetime) or in functions that accept callbacks which must work with references of arbitrary lifetimes.
Question H5: Variance
What is variance in Rust (covariant, contravariant, invariant)? Give an example with references.
Show answer
Variance describes how generics react to subtypes (lifetime subtyping):
&Tis covariant in T: you can widen the lifetime (use a&'long strwhere&'short stris expected).&mut Tis invariant: you cannot widen or narrow anything.fn(T)is contravariant in T.
Example: &'static str can be used where &'a str is expected (covariance), but &mut &'static str CANNOT be used where &mut &'a str is expected (invariance).
Question H6: Memory ordering
In std::sync::atomic, what is the difference between Relaxed, Acquire, Release, and SeqCst? When should you choose Relaxed over SeqCst?
Show answer
Relaxed: no ordering guaranteed between operations, just atomicity of the operation itself.Acquire: guarantees that subsequent reads see the writes preceding the corresponding Release.Release: guarantees that preceding writes are visible to a subsequent Acquire.SeqCst: total global ordering, the strongest but most expensive in terms of performance.
Relaxed is used when only the counter matters and you do not need to synchronize other data (e.g., metrics, statistics counters).
Question H7: Zero-cost abstractions
Explain what Rust means by "zero-cost abstractions." Give a concrete example.
Show answer
Rust compiles its abstractions via LLVM into machine code as efficient as hand-written C, with no runtime overhead.
Example: a for x in 0..n compiles to a simple add/jmp loop identical to C. Chained iterators (map/filter/fold) are also optimized into a single loop thanks to monomorphization and compiler inlining.
Question H8: FFI (Foreign Function Interface)
How do you secure calling a C function from Rust via extern "C"? What are the risks?
Show answer
You declare via extern "C" { fn foo(...); }. The call is mandatory inside an unsafe block. To secure it: create a safe wrapper with Rust types, verify ABI compatibility (#[repr(C)]), and manage memory manually.
Main risks: invalid pointers, double-free, panic crossing the FFI boundary (undefined behavior), and ABI incompatibility between Rust and C.
Question H9: No-std environments
What does compiling a crate with #![no_std] mean? What are the impacts?
Show answer
#![no_std] means you do not import the standard library, so no dynamic allocation and no OS dependency. You keep core (fundamental types and traits) and optionally alloc (if an allocator is available).
This is used for embedded and bare-metal development. Typical crates in this ecosystem: embedded-hal, cortex-m, heapless.
Question H10: Miri and Undefined Behavior (UB)
What is cargo miri and how can it detect errors in compilable Rust code? Give an example of UB possible despite the borrow checker.
Show answer
cargo miri is an interpreter that executes Rust code and checks for undefined behavior at runtime: invalid memory access, data races, aliasing violations, buffer overflows.
Unlike the compiler which performs static verification, Miri simulates actual execution and can detect UB in unsafe code that rustc cannot predict.
Example of UB despite the borrow checker: using unsafe with two mutable references to the same object:
fn main() { let mut x = 42; let r: *mut i32 = &mut x; unsafe { *r = 43; let p = r.offset(1); // out of bounds *p = 99; // UB not detected by rustc, detected by Miri } }
Commands: cargo miri setup, cargo miri run, cargo miri test.
Bonus: Closures and function traits
Question B1: What is the fundamental difference between Fn, FnMut, and FnOnce?
Show answer
Fn: immutable closure, can be called multiple times, does not affect its environment.FnMut: mutable closure, can modify captured variables, can be called multiple times.FnOnce: closure that consumes its environment, can only be called once.
Relationship: Fn also implements FnMut and FnOnce. Any Fn can be used where an FnMut or FnOnce is expected.
Question B2: Why clone before a closure with move?
Show answer
When you declare a closure with move, Rust moves the captured variables into the closure. If you need the original variable afterward, you clone before:
let s = String::from("hello"); let c = { let s = s.clone(); move || println!("{}", s) }; // s is still accessible here c(); // uses the copy
This is a classic pattern for isolating the closure from external code, especially in multi-threaded or async contexts.
Question B3: Send and Sync in one sentence?
Show answer
Send means you can move the value between threads. Sync means you can share immutable references between threads safely.
Key point: Rc<T> is neither Send nor Sync; use Arc<T> for multi-threading. Mutex<T> is Sync if T is Send.