So, you’ve:
Huzzah! You’re almost there and ready to be hired as a Rust Developer.
All that’s left is to prepare for your interviews, answer their questions with ease and then start earning that 100K a year salary.
Here’s the thing though…
During the initial part of your Rust interview, the interviewer will often try to determine if you actually write Rust code regularly, and so they will ask you some preliminary ‘Rust-focused’ questions before you get into the interview proper.
Think of it as a BS filter to see if you can really do what you say you can.
The last thing you want is to fail at this first step, so I’ve put together 53 basic to advanced Rust interview questions for you to practice and learn from.
Each of these questions covers topics or situations that should occur on an almost daily basis when programming in Rust.
So if you know what you’re doing, you should be able to answer these easily.
Think of this as a great way to test your level of knowledge in Rust.
Important: Some of these questions may seem simple or even obvious if you’ve learned how to use Rust correctly. If that's the case, the extra confidence you have from feeling this way will help you.
Being able to answer all of these questions with confidence will set the tone for the remainder of the interview, and it can be the difference in a successful or anxious interview experience.
That being said, if you find yourself having trouble answering these questions as you read through them, simply take some time to write some more Rust code, fill out any gaps in your knowledge, and then return to the questions and try again.
But if you really struggle and they don’t make any sense?
Well then you probably didn't learn rust from my complete Rust programming course 😉. It might be worth checking out!
Because I'm a generous guy, I've also put the first 6 hours of my course for free on the ZTM Youtube channel. You can watch it below:
Anyways, that’s enough preamble. Let’s dive into the questions!
I’ve broken these down into 3 sections for you:
Rust is a general-purpose memory-safe high-performance systems programming language.
It enables developers to write correct and maintainable code. Rust can also compile programs for different architectures and systems, including web browsers.
Rust has high performance because it compiles directly to native machine code. This enables the program to run at full speed since there isn't an interpreter needed to translate the program to machine instructions.
Rust allocates the minimum amount of memory required for an operation and only does so when needed. Once the operation finishes, the memory is then deallocated.
This is in contrast to garbage-collected languages where memory may remain allocated until the garbage collector has an opportunity to deallocate the memory.
To use cargo
to build Rust code, the build
command gets used:
cargo build
When using cargo build
, the --release
flag will build in release mode.
This turns on optimizations and does not include debug code, which makes the compiled program run at its intended speed.
To use cargo
to test Rust code, the test
command gets used:
cargo test
After running cargo test
, a debug version of the program gets built and then the test suite runs.
The results of the tests get displayed as they run, and are marked with a pass or fail. If the test fails, an error message will indicate the cause of the failure.
Rust is a general-purpose programming language that is suitable for writing different kinds of programs across a large domain.
Using Rust, you can create web servers, command line programs, databases, audio plugins, text processors, operating systems, device drivers, and more.
Rust's high performance makes it appealing to use when writing real-time applications such as video and audio decoding.
Rust is also a secure programming language, making it a good choice to write software that demands a high level of availability and security, such as server software or cryptographic algorithm implementations.
enum
and struct
?While both enum
and struct
provide ways to encapsulate data, they do so in different ways.
A struct
contains fields and every field in the struct
is present at all times. This makes struct
appropriate when you need to group data together and have access to all components of that data.
An enum
contains variants in which a single variant gets represented at a time.
This makes enum
appropriate when you have more than one data component, but only want a single component at a time.
A parser is an example where using an enum
makes sense because a token may be one of a predefined number of items.
Learn more about Enums in Rust here.
impl
block in RustAn impl
block allows implementing functionality on a Rust enum
or struct
.
When functionality gets implemented in this way, the functionality becomes bound to the enum
or struct
. This helps to encapsulate functionality that is specific to the given enum
or struct
.
Here is an example of an impl
block in Rust implementing functionality to create a new struct
:
struct Number(i32);
impl Number {
pub fn new(n: i32) -> Self {
Self(n)
}
}
let five = Number::new(5);
Using the Rust keyword loop
will create an infinite loop:
loop {
// ...
}
Using the break
keyword will exit the loop.
By default, all data in Rust is immutable and cannot get changed without being marked as mutable.
Using the mut
keyword changes this behavior and allows changing (mutating) the data:
let mut a = 0; // mutable
let b = 0; // immutable
When writing a function that borrows data, the borrowed data will remain available for use after the function ends. This is because ownership of the data does not transfer when borrowed.
When writing a function that takes ownership of data, the data gets dropped (deleted) at the end of a function.
This is because all owned data gets dropped at the end of a scope, and the end of a function marks the end of a scope.
#[derive(Debug)]
do in Rust?Using #[derive(Debug)]
allows a struct
or enum
to get printed with the debug formatting token {:?}
in the println!
and format!
macros.
.unwrap()
and .expect()
in Rust?Both .unwrap()
and .expect()
will trigger a panic if they execute.
.unwrap()
triggers a thread panic and then displays the line number containing the call to .unwrap()
.
.expect()
triggers a thread panic with a customized message, and then displays the line number containing the call to .expect()
.
return
keyword in Rust optional? Provide examplesThe return
keyword is optional in Rust because Rust is an expression-based language.
Expressions get evaluated and then the result of the evaluation propagates outwards.
This is different when compared to other programming languages that use statements. Statements take some action and then nothing happens at the end of the statement. When using data with statements, extra keywords help to facilitate propagation.
Here is an example of a function that omits the return
keyword:
fn one() -> u32 {
1
}
Here is an example of a function that uses the return
keyword:
fn two() -> u32 {
return 2;
}
The return
keyword is for returning early from a function. If there isn't a need to return early, then the omitting return
keyword is appropriate. Leveraging expressions allows the Rust compiler to determine if the branches in the function are all handled properly.
match
expression in Rust?This Rust match
expression matches an Option
.
When the Option
contains Some
, the data gets printed to the terminal. When the Option
contains None
, the message there is no number
gets printed to the terminal.
let foo = Some(1);
match foo {
Some(n) => println!("number is {n}"),
None => println!("there is no number"),
}
match
expression to a variable binding?Yes. Since match
is an expression, assigning the result gets accomplished like this:
let t = true;
let one = match t {
true => 1,
false => 0,
};
enum
without changing any other code?Adding a new variant to an enum
without changing any other code may trigger compiler errors elsewhere in the program.
When using match
on an enum
, all variants must get checked.
Adding a new variant to the enum
, without updating the match
blocks which use the enum
, will trigger compiler errors. This is because the match
expression no longer handles all possible variants, but this applies solely when the match
block does not have a "catch-all" match arm.
Using the for
will iterate through a collection:
let nums = vec![1, 2, 3];
for n in nums {
println!("{n}")
}
In order for the iteration to occur, the collection must implement the Iterator
trait.
Using the println
macro will print information to the terminal:
println!("hello world");
The dbg!
macro is also capable of printing information to the terminal, but should get used solely for debugging purposes:
let life = 42;
dbg!(life);
Vec
and when would you use it?A Vec
is a linear collection of elements, with similarities to a dynamic array present in other languages.
Vec
allows iteration over elements, indexing into the Vec
, retrieving elements at a given index, and much more.
You would use a Vec
when you need to store elements in a defined order, or when you plan on iterating over the elements.
Still with me? Easy peasy?
These types of questions will help the interviewer establish that you have actually programmed using Rust before.
Let's ramp up the difficulty a bit.
These questions would get a sense of how deep your Rust knowledge is.
You may not know the answers to some of these questions but even getting most of the questions correct means you're probably "ready" for a Rust interview.
Yes, to create more than one variable in one line of Rust code, a destructuring operation needs to occur.
The following code will create variables a
and b
by destructuring the tuple (1, 2):
let (a, b) = (1, 2);
It's not possible to declare more than one uninitialized variable in a single line of code.
Traits in Rust provide a way to declare that some behavior exists.
The implementation of that behavior is specific to the data that implements the trait. It's like creating an interface where the interface dictates what can happen and the implementation dictates how it happens.
Rust generics provide a way to create structures
, enums
, or functions
that do not know what data they will be working with.
When used with generics, traits act as generic constraints, and these constraints declare what kinds of data may get used with the function.
Once the structure
, enum
, or function
uses some data, the compiler will check that the data implements the required traits indicated in the generic constraints.
If the data meets all the requirements, then it gets accepted. If not, a compiler error occurs.
Using borrowed data within a Rust structure requires the use of lifetime annotations.
Lifetime annotations tell the compiler that we are borrowing some data from another part of the program:
#[derive(Debug)]
struct Name<'a> {
name: &'a str,
}
let name = String::from("Bob");
let n = Name { name: &name };
In the above example, the Name
structure borrows a &str
.
The lifetime of the &str
is 'a
. Seeing a lifetime in a struct informs developers that some data needs to already exist before creating the structure.
Yes, Rust support continuing an outer loop when executing an inner loop through the use of loop labels:
let mut a = 0;
'outer: loop {
a += 1;
let mut b = 0;
loop {
if b == 3 {
continue 'outer;
}
b += 1;
}
}
Using loop labels with the break
keyword instead of continue
will enable an inner loop to exit both an inner and outer loop.
The question mark operator in Rust offers a convenient way to handle errors or missing data.
When used with Result
, the question mark operator will either:
Ok
return
an Err
When it's used with Option
, it will either:
Some
return
a None
Because the question mark operator potentially returns values, the function signature must have either Result
or Option
set as the return type.
This Rust function is generic over all types that implement the std::fmt::Debug
trait, which allows them to print in a debug context:
fn debug_print<T: std::fmt::Debug>(t: T) {
println!("{t:?}");
}
#[derive(Debug)]
struct Sample;
struct Whoops;
debug_print(Sample);
debug_print("test");
// debug_print(Whoops); // compiler error
In the last line of the example the Whoops
data structure does not implement Debug
. This line will trigger a compiler error since it does not meet the constraints set by the debug_print
function.
This generic Rust structure wraps a Vec
and exposes a single push
function which allows pushing data to the inner Vec
.
It has no generic constraints, so it operates on all types:
struct Container<T> {
inner: Vec<T>,
}
impl<T> Container<T> {
pub fn push(&mut self, item: T) {
self.inner.push(item);
}
}
let mut container = Container { inner: vec![] };
container.push("sample");
Here is an example of creating and implementing a trait in Rust:
trait Speak {
fn speak();
}
struct Dog;
impl Speak for Dog {
fn speak() {
println!("bark bark!")
}
}
The speak
function on the Speak
trait doesn't get defined, making it a required function to implement. The Dog
structure implements the Speak
trait by printing bark bark!
whenever the function gets called.
Rust has three different types of closures: Fn
, FnOnce
, and FnMut
.
Fn
closures can get called any number of times and they operate solely on immutable data.
FnOnce
closures can get called a single time. This happens if a closure moves data out of its body.
FnMut
closures can get called any number of times and may mutate captured data.
String
and &str
?A Rust String
is an owned string that is heap-allocated, while a &str
is a borrowed data type.
Since String
is an owned data type, the methods implemented on it focus on manipulation of the String
contents.
&str
is a borrowed data type, so the implemented functionality focuses on reading and searching the string data.
The type state pattern utilizes the type system in Rust to define a state machine.
Each state in the state machine gets represented with a Rust struct
, and transitions get represented using function calls. These function calls return the defined state structs
and are the sole points where transitions occur.
This prevents outside code from both instantiating an incorrect state machine and performing incorrect state transitions.
The new type pattern in Rust takes an existing type and wraps it in a type created by the developer.
The purpose of using the new type pattern is to implement traits on existing types and to provide interfaces that are relevant to the application.
.iter()
and .into_iter()
?.iter()
creates an iterator over a collection.
The items produced by the iterator get borrowed from the original collection, leaving it intact. This is useful when you need to keep a copy of the original data.
.into_iter()
also creates an iterator over a collection, but instead takes ownership of the collection and moves the items out of the collection.
This is useful when the original data is no longer needed, or if the data needs to be moved to another location (like into a thread).
Option
into a Result
?The most succinct way to convert an Option
into a Result
is to use .ok_or_else()`:
let foo: Option<i32> = Some(1);
let foo: Result<i32, &str> = foo.ok_or_else(|| "no number provided");
The .ok_or_else()
method will convert an Option
into a Result
.
When the Option
is None
, then the closure provided to .ok_or_else()
gets run, and the result from running the closure gets wrapped within the Err
variant of Result
.
Result
into an Option
in Rust?Converting a Result
into an Option
gets accomplished using the .ok()
method available on ```Result`:``
let foo: Result<i32, ()> = Ok(1);
let foo: Option<i32> = foo.ok();
Since the None
variant on Option
doesn't have any data associated with it, converting a Result
into an Option
discards all error information (if any) contained within the Err
variant of the Result
.
.map()
function on Rust's Iterator
traitThe .map()
function on Iterator
performs a transformation on all items within the iterator. The input to .map()
is the item in the current iteration, and the output from .map()
is the transformed item.
Here is an example that iterates over some numbers and uses .map()
to double the value of each number:
let nums = vec![1, 2, 3];
let doubled = nums.iter().map(|n| n * 2):
Vec
?Converting a Rust iterator into a Vec
makes use of the````.collect()``` function:
let nums = vec![1, 2, 3];
let doubled = nums.iter().map(|n| n * 2).collect::<Vec<_>>();
The collect
function "collects" all the items in the iterator and creates a new data structure containing the items. This works solely on data structures that implement the Iterator
trait.
HashMap
in Rust and when would you use it?A HashMap
is a collection consisting of key/value pairs.
The keys get used to locate elements within the HashMap
, and the values are the data associated with each key.
Since HashMap
uses key accesses, it's a great data structure to use when you want to randomly access data and you have the key available.
Learn more about HashMap's in this video here.
Creating a nested function in Rust is the same as creating a non-nested function. Use the fn
keyword within an existing function to create a nested one:
fn outer() -> bool {
fn inner() -> bool {
true
}
inner()
}
Nested functions are great to use when you want to avoid repeating some code, but the scope of the function isn't useful enough to exist at the module level.
Using a nested function can enable you to encapsulate the functionality without resorting to extra modules.
You can also learn more about functions in Rust here.
The interviewer may create a scenario and ask what you would do in that scenario, or they may ask you to walk them through a difficult situation you encountered in your own projects.
Not only that, but they will also ask detailed technical questions related to the kind of work they do in the company, and how that works within the Rust programming language.
Because the questions can be about anything, there's a good chance that you may not know the answers. The other key difference is that there isn't necessarily a black and white answer.
So the most important part that they'll be evaluating you on is how you answer these type of questions to get a sense for your thought process and how you would work through a problem.
Below are some example scenarios…
During code compilation, the question mark operator gets converted into a match
expression. In the Err
arm, the Into
trait gets used to convert the error into the appropriate type.
Here is an example of what code the question mark operator generates:
let foo = bar()?;
let foo = match bar() {
Ok(f) = f,
Err(e) => return Err(e.into())
};
String
, &str
, Path
, and PathBuf
using a single parameterA Rust function that accepts different types using a single function parameter requires the use of generics. Here is an example:
fn sample<P: AsRef<Path>>(p: P) { }
The AsRef
trait can convert String
, &str
, and PathBuf
to a Path
because there are implementations of AsRef
on these types to perform the conversion.
There are two ways to create an iterator when working with your own Rust data structures.
If the data structure contains another data structure that implements Iterator
, then using the other data structure's .iter()
method is a quick way to enable iteration.
For example:
struct Foo {
inner: Vec<usize>,
}
impl Foo {
pub fn iter(&self) -> impl Iterator<Item = &usize> {
self.inner.iter()
}
}
If there is no inner data structure that implements Iterator
, then Iterator
needs to get implemented.
Here is an example of an iterator which that computes fibonacci numbers:
struct Fibonacci {
n: u64,
}
impl Fibonacci {
pub fn new(n: u64) -> Self {
Self { n }
}
}
impl Fibonacci {
fn fibonacci(n: u64) -> u64 {
match n {
0 => 1,
1 => 1,
_ => Self::fibonacci(n - 1) + Self::fibonacci(n - 2),
}
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let next = Some(Self::fibonacci(self.n));
self.n += 1;
next
}
}
let fib = Fibonacci::new(0);
for f in fib {
// ...
}
Arc
in Rust gets used when multiple threads need access to some data.
For example
There can be some global configuration data that needs sharing across multiple threads. Using an Arc
allows all threads to access this data:
use std::{path::PathBuf, sync::Arc};
#[derive(Clone)]
struct Config {
path: Arc<PathBuf>,
}
Now that the ```path``` is protected by an ```Arc```, sharing the data is safe to do between different threads.
### #45. Is it possible to create a Rust ```Vec``` that contains different data types? Provide examples
Yes, different data types can exist within a single ```Vec``` in Rust.
The data must get converted into [trait objects](https://doc.rust-lang.org/book/ch17-02-trait-objects.html) and then accessed using [dynamic dispatch](https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch):
```rust
use std::fmt;
#[derive(Debug)]
struct Foo;
#[derive(Debug)]
struct Bar;
fn print(d: &dyn fmt::Debug) {
println!("{d:?}");
}
let items: Vec<Box<dyn fmt::Debug>> = vec![Box::new(Foo), Box::new(Bar)];
for i in items {
print(&i);
}
Using trait objects and dynamic dispatch incurs some overhead.
Trait objects get stored on the heap, and accessing them requires a pointer indirection. When running functions implemented on trait objects using dynamic dispatch, another pointer indirection occurs.
Compared to stack-allocated non-dynamic data, trait objects will be slower because of multiple pointer indirections and heap-only memory accesses.
A supertrait in Rust is a combination of two or more traits.
When a supertrait gets set as a trait bound, all traits that compose the supertrait require implementations on the type.
For example
Here is a supertrait composed of two traits:
trait Foo {
fn foo(&self);
}
trait Bar {
fn bar(&self);
}
// supertrait
trait FooBar: Foo + Bar{}
Here is a structure that implements the supertrait, along with a function using the composed trait's functionality:
struct A;
impl Foo for A {
fn foo(&self) {
println!("A foo")
}
}
impl Bar for A {
fn bar(&self) {
println!("A bar")
}
}
impl FooBar for A {}
fn foobar(f: impl FooBar) {
f.foo();
f.bar();
}
fn main() {
let a = A;
foobar(a);
}
Learn more about traits in this video here.
Declarative macros are useful in Rust when you need to write multiple blocks of code where each block contains similar code.
Examples of when to use declarative macros include writing impl
blocks, or encapsulating control flow.
It's also possible to use declarative macros to create domain-specific languages (DSL).
DSLs are useful because the syntax of the DSL is customizable when creating the macro. This can help make otherwise complicated code easier to work with.
This Rust declarative macro repeats an impl
block for each type provided:
trait Speak {
fn speak(&self);
}
macro_rules! impl_speak {
(
$( $type:ty => $msg:literal )+
) => {
$(
impl Speak for $type {
fn speak(&self) {
println!($msg);
}
}
)+
}
}
struct Dog;
struct Cat;
struct Bird;
impl_speak! {
Dog => "bark bark"
Cat => "meow"
Bird => "tweet tweet"
}
The Rust generic type state pattern is useful when you want to preserve data across multiple states.
In this example, a cruise control system for a car can transition between On
, Off
, and Suspended
.
The cruise control speed remains available across different states during transitions:
struct Speed(u32);
// Allow adding `Speed`
impl std::ops::AddAssign for Speed {
fn add_assign(&mut self, rhs: Self) {
self.0.saturating_add(rhs.0);
}
}
// Allow subtracting `Speed`
impl std::ops::SubAssign for Speed {
fn sub_assign(&mut self, rhs: Self) {
self.0.saturating_sub(rhs.0);
}
}
// trait that all states must implement
trait Cruising {}
// states
struct Off;
struct On;
struct Suspended;
// enable usage in the state container
impl Cruising for Off {}
impl Cruising for On {}
impl Cruising for Suspended {}
// state container
struct CruiseControl<T: Cruising> {
// current state
state: T,
/// target cruising speed
target: Speed,
}
// transition function usable by all states
impl<T: Cruising> CruiseControl<T> {
fn transition<N: Cruising>(self, next: N) -> CruiseControl<N> {
CruiseControl {
target: self.target,
state: next,
}
}
}
impl CruiseControl<Off> {
fn engage(target: Speed) -> CruiseControl<On> {
CruiseControl { state: On, target }
}
}
impl CruiseControl<On> {
pub fn speed_increase(&mut self, amount: Speed) {
self.target += amount;
}
pub fn speed_decrease(&mut self, amount: Speed) {
self.target -= amount;
}
pub fn suspend(self) -> CruiseControl<Suspended> {
self.transition(Suspended)
}
pub fn disengage(self) -> CruiseControl<Off> {
self.transition(Off)
}
}
impl CruiseControl<Suspended> {
pub fn resume(self) -> CruiseControl<On> {
self.transition(On)
}
pub fn resume_at_target(self, target: Speed) -> CruiseControl<On> {
// update to new target
let mut control = self;
control.target = target;
control.transition(On)
}
pub fn disengage(self) -> CruiseControl<Off> {
self.transition(Off)
}
}
fn sample() {
let mut i = 0;
std::thread::spawn(|| {
i += 1;
});
}
This Rust code fails to compile because the sample
function has ownership of the i
variable.
When the sample
function ends, the i
variable will get destroyed. The thread spawned may continue to live even though the sample
function is complete and destroyed i
.
For this reason, it would be unsafe to mutate i
since it may no longer exist by the time the thread gets the opportunity to make any updates to the variable.
To fix this error, i
has to move into the thread:
fn sample() {
let mut i = 0;
std::thread::spawn(move || {
i += 1;
});
}
Once i
gets moved into the thread, the sample
function no longer has ownership of i
and cannot delete it. This enables the thread to mutate i
.
git
repository instead of a crate registry?In the Cargo.toml
file, a dependency can be set to use a git repository by using the git
key:
[dependencies]
serde = { git = "https://github.com/serde-rs/serde", features = ["derive"]}
Cargo workspaces provide a way to group crates under a single directory and have them get managed by cargo.
This allows using a single cargo command to build and test the crates in the workspace without the need to jump between different projects. The crates all use a single target
directory, sharing artifacts across projects.
To make a workspace, create a Cargo.toml
in an empty directory with the following content:
[workspace]
members = [
"crate_one",
"another_crate",
"three"
]
Each item in the members
array should be a folder containing a Rust crate with its own Cargo.toml
file.
After creating this file, cargo
commands will automatically operate on the entire workspace.
Did you get all 53 correct? Hopefully these questions help you to prepare for your Rust interview.
If in doubt, remember to always answer interview questions with confidence, and if you don't know the answer, use your experience to talk through how you're thinking about the problem and ask follow-up questions if you need some help.
This isn't a bad thing. Even the most senior programmers have to ask questions and google things.
The point is to show the interviewer that you can think through situations instead of giving up and saying "I don't know". They don't expect you to know 100%, but they do want people who can adapt and figure things out.
Someone who puts in the effort to answer questions outside their scope of knowledge is much more likely to get hired than someone who gives up at the first sign of trouble.
Good luck and be sure to message me when you secure that new job!
P.S: If the majority of this made no sense to you, then I highly recommend taking my complete Rust Developers course.
It will walk you through learning Rust in a logical progression. You'll work through all the elements we’ve talked about in this guide and get to build some cool projects along the way.
If you've made it this far, you're clearly interested in Rust so definitely check out all of my Rust posts and content: