Rust has been Stack Overflow’s most loved language for four years in a row and emerged as a compelling language choice for both backend and system developers, offering a unique combination of memory safety, performance, concurrency without Data races, and modern language features. Come along and discover the 27 most common Rust interview questions and answers to crack your next backend or system software developer interview.
Lifetimes are what the Rust compiler uses to keep track of how long references are valid for. Checking references is one of the borrow checker’s main responsibilities.
Lifetimes help the borrow checker ensure that you never have invalid references.
In many cases, the borrow checker can infer the correct lifetimes and take care of everything on its own. Lifetime annotations enable you to tell the borrow checker how long references are valid for.
Here are some key features and concepts of Rust:
String and str in Rust?String is the dynamic heap string type, like Vec: use it when you need to own or modify your string data. String is like a std::string in C++; it owns the memory and does the dirty job of managing memory.str is an immutable sequence of UTF-8 bytes of dynamic length somewhere in memory. Since the size is unknown, one can only handle it behind a pointer. This means that str most commonly appears as &str: a reference to some UTF-8 data, normally called a "string slice" or just a "slice".&str is like a char* in C++ (but a little more sophisticated); it points us to the beginning of a chunk in the same way you can get a pointer to the contents of std::string.Consider:
fn main() {
let mut owned_string: String = "hello ".to_owned();
let borrowed_string: &str = "world";
owned_string.push_str(borrowed_string);
println!("{}", owned_string);
}In summary, use String if you need owned string data (like passing strings to other threads, or building them at runtime), and use &str if you only need a view of a string.
String to a function in Rust?In Rust, each value has a single owner at any given time. When you pass a String as an argument to a function, the ownership of the String is moved from the caller to the function. This means the caller no longer has access to the original String after the function call. This is known as "moving" the String, and it's part of Rust's ownership system.
fn main() {
let original_string = String::from("Hello");
let modified_string = append_world(original_string); // Ownership is moved to the function.
// The following line will cause a compilation error since `original_string` is no longer valid.
// println!("Original: {}", original_string);
println!("Modified: {}", modified_string); // This works since `modified_string` now owns the value.
}
fn append_world(mut input: String) -> String {
input.push_str(", world!");
input // Ownership is moved back to the caller.
}The borrow checker is a fundamental part of Rust's ownership system that enforces rules to prevent data races and memory safety issues. It ensures that references to data (borrowed references) are used correctly and that multiple references don't lead to data inconsistencies. The borrow checker analyzes your code statically (at compile-time) and guarantees memory safety without the need for garbage collection or runtime checks.
The borrow checker analyzes the code at compile-time to ensure that references adhere to the following rules:
&mut T) or multiple immutable references (&T) to a particular data at any given time.Here's an example to illustrate how the borrow checker works:
fn main() {
let mut value = 42;
let reference1 = &value; // Immutable borrow.
let reference2 = &value; // Another immutable borrow.
println!("References: {}, {}", reference1, reference2);
// This line will cause a compilation error because we can't have a mutable borrow
// while there are active immutable borrows.
// let mutable_reference = &mut value;
// After the last use of the immutable references, we can safely obtain a mutable borrow.
let mutable_reference = &mut value;
*mutable_reference += 10;
println!("Modified value: {}", value);
}A lifetime is how long the data a pointer points to is guaranteed to exist, e.g. a global variable is guaranteed to last "forever" (so it's got the special lifetime 'static).
One neat way to look at them is:
Every reference in Rust has a lifetime, which is the scope for which that reference is valid. Most of the time lifetimes are implicit and inferred.
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} The outer scope declares a variable named r with no initial value, and the inner scope declares a variable named x with an initial value of 5. Inside the inner scope, we attempt to set the value of r as a reference to x. Then the inner scope ends, and we attempt to print the value in r. This code won’t compile because the value r is referring to has gone out of scope before we try to use it.
Option type in Rust, and why is it useful?In Rust, the Option type is an enumeration (enum) that represents the presence or absence of a value. It's a fundamental part of Rust's approach to handling potentially nullable or absent values in a type-safe and idiomatic manner. The Option enum has two variants:
Some(T): Represents a value of type T. It indicates that a value is present.None: Represents the absence of a value. It's similar to null or nil in other languages.Rc<T> vs Arc<T> in RustBy default in Rust, every piece of data has a single owner, and when that owner goes out of scope, the data is cleaned up.
The types Rc<T> and Arc<T> (short for ‘Reference counted’ and ‘Atomically reference counted’, respectively) give shared ownership of the contained data by tracking the number of references and ensuring the data will last as long as there are any active references:
Clone and Drop: cloning increments the reference count while dropping one decrements itRc and Arc only allow you to get immutable references to the underlying dataArc is thread-safe, Rc isn'tArc uses atomic operations to manage the reference count, which gives it a higher runtime cost but makes it safe to share between threadsRc is the faster alternative if you’re only working in a single threadIn this example, the CustomFormat trait is defined with a single method custom_format that takes a separator as an argument. The impl block provides a generic implementation of CustomFormat for any type that implements the Display trait and can be converted into an iterator. Inside the implementation, the items are formatted using the format! macro and concatenated with the separator.
use std::fmt::{Display, Formatter, Result};
trait CustomFormat {
fn custom_format(&self, separator: &str) -> String;
}
impl<T: Display, I: IntoIterator<Item = T>> CustomFormat for I {
fn custom_format(&self, separator: &str) -> String {
let mut result = String::new();
let mut iter = self.into_iter();
if let Some(item) = iter.next() {
// Format the first item
result.push_str(&format!("{}", item));
// Format the remaining items with the separator
for item in iter {
result.push_str(&format!("{}{}", separator, item));
}
}
result
}
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let formatted = numbers.custom_format(", ");
println!("Formatted: {}", formatted);
}Object-oriented programming (OOP) in Rust is achieved through a combination of the language's core features, including struct, enum, impl blocks, and traits. While struct and enum are related to defining data structures, trait is a way to define behavior that can be shared among different types.
struct type can be used to define data structures, similar to classes in traditional OOP languages.Traits in Rust are akin to interfaces in other languages. They define a set of methods that types can implement to provide a common behavior.Consider:
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
fn main() {
let circle = Circle { radius: 5.0 };
println!("Circle area: {}", circle.area());
}Ownership is Rust’s most unique feature. It enables Rust to make memory safety guarantees without needing a garbage collector.
Every value in Rust has a variable that is known as its owner. There can only be one owner at a time that is dropped when it goes out of scope. Dropping is equivalent to cleaning the memory allocated on the heap when it can no longer be accessed.
When doing assignments (let x = y) or passing function arguments by value (foo(x)), the ownership of the resources is transferred. In Rust-speak, this is known as a move.
After moving resources, the previous owner can no longer be used. This avoids creating dangling pointers.
fn main() {
let x = String::from("Hello World!"); // x is the first owner
let y = x; // y becomes the new owner
println!("y = {}", y); // Using 'x' will give an error
}Trait in Rust? What are some use cases when you need to implement one?Traits in Rust are similar to interfaces in other programming languages but with some additional capabilities. In Rust, a trait is a language construct that defines a set of methods that can be implemented by types (structs, enums, or other traits). Traits allow you to define shared behavior for different types and enable a form of ad hoc polymorphism called trait polymorphism or trait-based dispatch.
Consider:
// Define a trait named Printable with a single method print().
trait Printable {
fn print(&self);
}
// Implement the Printable trait for i32.
impl Printable for i32 {
fn print(&self) {
println!("Value: {}", self);
}
}
// Implement the Printable trait for String.
impl Printable for String {
fn print(&self) {
println!("String: {}", self);
}
}
// A generic function that accepts any type that implements the Printable trait.
fn print_generic<T: Printable>(value: T) {
value.print();
}
fn main() {
let num = 42;
let text = String::from("Hello, trait!");
print_generic(num); // Output: Value: 42
print_generic(text); // Output: String: Hello, trait!
}You can use traits for:
In Rust, you can use the Rc (Reference-Counted) smart pointer to create shared ownership among multiple references. Here's how you might model a many-to-many relationship using Rc:
use std::rc::Rc;
struct Student {
name: String,
}
struct Course {
title: String,
students: Vec<Rc<Student>>,
}
fn main() {
let alice = Rc::new(Student { name: "Alice".to_string() });
let bob = Rc::new(Student { name: "Bob".to_string() });
let math = Course {
title: "Math".to_string(),
students: vec![alice.clone(), bob.clone()],
};
let physics = Course {
title: "Physics".to_string(),
students: vec![alice.clone()],
}
}String type in Rust Copy or Clone?A simple bitwise copy of String values would merely copy the pointer, leading to a double free down the line. The implementation of Clone for String. needs to copy the pointed-to string buffer in the heap.
For this reason, String is Clone but not Copy.
Languages with a garbage collector periodically scan the memory (one way or another) to find unused objects, release the resources associated with them, and finally release the memory used by those objects.
Rust would know when the variable gets out of scope or its lifetime ends at compile time and thus insert the corresponding LLVM/assembly instructions to free the memory. That behavior is called ownership. Rust is using an affine type system, it tracks which variable is still holding onto an object and, when such a variable goes out of scope, calls its destructor.
This ownership works recursively: if you have a Vec<String> (i.e., a dynamic array of strings), then each String is owned by the Vec which itself is owned by a variable or another object, etc... thus, when a variable goes out of scope, it recursively frees up all resources it held, even indirectly. In the case of the Vec<String> this means:
StringVec itselfThus, thanks to the ownership tracking, the lifetime of ALL the program objects is strictly tied to one (or several) function variables, which will ultimately go out of scope (when the block they belong to ends).
Copy and Clone in Rust?Copy trait represents values that can be safely duplicated via memcpy: things like reassignments and passing an argument by-value to a function are always memcpys, and so for Copy types, the compiler understands that it doesn't need to consider those as a move.// u8 implements Copy
let x: u8 = 123;
let y = x;
// x can still be used
println!("x={}, y={}", x, y);Clone is designed for arbitrary duplications (either a deep or shallow copy): a Clone implementation for a type T can do arbitrarily complicated operations required to create a new T. It is a normal trait (other than being in the prelude), and so requires being used explicitly like a normal trait, with method calls, etc.// Vec<u8> implements Clone, but not Copy
let v: Vec<u8> = vec![1, 2, 3];
let w = v.clone();
//let w = v // This would *move* the value, rendering v unusable.For your own types, .clone() can be an arbitrary method of your choice, whereas implicit copying will always trigger a memcpy, not the clone(&self) implementation.
iter and into_iter in Rust?An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished.
Consider:
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {}", val);
}iter() iterates over the items by reference. Use the iter() function if you want to iterate over the values by reference.iter_mut() iterates over the items, giving a mutable reference to each item. Use it if you do find yourself wanting to mutate some data.into_iter() iterates over the items, moving them into the new scope. Use the into_iter() function when you want to move, instead of borrow, your value.So:
for x in my_vec { ... } is essentially equivalent to my_vec.into_iter().for_each(|x| ... ) - both move the elements of my_vec into the ... scope. iter, if you need to edit/mutate it, use iter_mut, and into_iter.self and Self in Rust?self when used as first method argument, is a shorthand for self: Self. There are also &self, which is equivalent to self: &Self, and &mut self, which is equivalent to self: &mut Self.
Self in method arguments is syntactic sugar for the receiving type of the method (i.e. the type whose impl this method is in). This also allows for generic types without too much repetition.
trait Clone {
fn clone(&self) -> Self;
}
impl Clone for MyType {
// I can use either the concrete type (known here)
fn clone(&self) -> MyType;
// Or I can use Self again, it's shorter after all!
fn clone(&self) -> Self;
}Copy in Rust?Some types can’t be copied safely. For example, copying &mut T would create an aliased mutable reference. Copying String would duplicate responsibility for managing the String’s buffer, leading to a double free.
Generalizing the latter case, any type implementing Drop can’t be Copy, because it’s managing some resource besides its own size_of::<T> bytes.
If you try to implement Copy on a struct or enum containing non-Copy data, you will get the error E0204.
By default, the Rust compiler optimizes for execution speed, compilation speed, and ease of debugging (by including symbols, for example), rather than minimal binary size.
Rust uses static linking to compile its programs, meaning that all libraries required by even the simplest Hello world! program will be compiled into your executable. This also includes the Rust runtime.
The current high level steps to reduce binary size are:
jemalloc by default)Cargo.toml:[profile.release]
opt-level = 'z' # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
strip = true # Strip symbols from binary**
strip = truerequires Rust 1.59+. On older Rust versions, runstripmanually on the resulting binary.
cargo build --release-C prefer-dynamic; this will result in a much smaller file size but will also require the Rust libraries (including its runtime) to be available to your program at runtime.strip target/release/<binary>The stat:
Fn/FnMut/FnOnce family of traits in Rustasync/.await in RustSend and Sync in Rust and when do you need them?Fn, FnMut and FnOnce traits?Rust has been Stack Overflow’s most loved language for four years in a row and emerged as a compelling language choice for both backend and system developers, offering a unique combination of memory safety, performance, concurrency without Data races...
Clean Architecture provides a clear and modular structure for building software systems, separating business rules from implementation details. It promotes maintainability by allowing for easier updates and changes to specific components without affe...
Azure Service Bus is a crucial component for Azure cloud developers as it provides reliable and scalable messaging capabilities. It enables decoupled communication between different components of a distributed system, promoting flexibility and resili...