Rust Interview Questions and Answers
Practice Rust interview questions and answers organized by difficulty, with revealable explanations and related topics.
Junior questions
12 questions- Question 1Junior
What are some some key features and concepts of Rust?
- Rust
Reveal answerHide answer
Here are some key features and concepts of Rust: Memory Safety: Rust's most distinctive feature is its system, which enables fine-grained control over memory allocation and deallocation. It ensures memory safety without relying on garbage collection. The ownership system prevents common issues like null pointer dereferences, dangling pointers, and data races. Concurrency: Rust provides powerful concurrency primitives, including threads, message passing, and shared-state concurrency. It enforces strict rules at compile-time to prevent data races and ensures thread safety. Performance: Rust is designed for high-performance systems programming. It provides control over low- level details while ensuring safety. Its zero-cost abstractions allow high-level code to have the same performance as equivalent low-level code. Expressive Type System: Rust has a strong and expressive type system. It includes features like pattern matching, algebraic data types, and generics, allowing developers to write generic and reusable code. The type inference system reduces the need for explicit type annotations. Error Handling: Rust has a robust error handling mechanism based on the "Result" type and the "Option" type. This approach encourages developers to handle errors explicitly and helps avoid unexpected runtime errors. Cargo: Rust's package manager and build system, called Cargo, simplifies dependency management and building projects. It provides tools for testing, documentation generation, and benchmarking, making it easier to develop and maintain Rust projects.
- Question 1Junior
What is a Lifetime in Rust?
- Rust
Reveal answerHide answer
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: lifetimes connect data to the stack frame on which their owner is placed; once that stack frame exits, the owner goes out of scope and any pointers to/into that value/data structure are no longer valid, and the lifetime is a way for the compiler to reason about this. when an instance is borrowed (a reference is taken), it is borrowed for a specific lifetime. The borrow checker will try to minimize the lifetime of the borrow so that it can be as permissive as possible whilst ensuring the code is still safe. 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.
- Question 2Junior
What happens when you pass a String to a function in Rust?
- Rust
Reveal answerHide answer
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. } - Question 3Junior
What is a Borrow Checker in Rust?
- Rust
Reveal answerHide answer
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:
- There can be either one mutable reference ( &mut T ) or multiple immutable references ( &T ) to a particular data at any given time.
- References must have a limited lifetime and cannot outlive the data they refer to. 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); } - Question 4Junior
What’s a closure in Rust?
- Rust
Reveal answerHide answer
A closure is a function that can directly use variables from the scope in which it is defined. This is often described as the closure closing over or capturing variables (the captures). Collectively, the variables are called theenvironment. Syntactically, a closure in Rust is an anonymous function defined a little like Ruby, with pipes: |arguments...| body . For example, |a, b| a + b defines a closure that takes two arguments and returns their sum.
- Question 5Junior
What are the differences between String and str in Rust?
- Rust
Reveal answerHide answer
String is the dynamic heap string type, like Vec : use it when you need to own or modify your string data. A Rust 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". A Rust &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 atruntime), and use &str if you only need a view of a string.
- Question 6Junior
Is there a way to directly convert a String to an int ?
- Rust
Reveal answerHide answer
You can directly convert to an int using the str::parse::() method, which returns a Result containing the int.
let my_string = "27".to_string(); // `parse()` works with `&str` and `String`! let my_int = my_string.parse::<i32>().unwrap(); You can either specify the type to parse to with the turbofish operator ( ::<> ) as shown above or via explicit type annotation: let my_int: i32 = my_string.parse().unwrap(); Since parse() returns a Result , it will either be an Err if the string couldn't be parsed as the type specified (forexample, the string "peter" can't be parsed as i32 ), or an Ok with the value in it.
- Question 7Junior
How to match a String against string literals in Rust?
- Rust
Reveal answerHide answer
Use .as_str() like this to convert the String to an &str :
match stringthing.as_str() { "a" => println!("0"), "b" => println!("1"), "c" => println!("2"), _ => println!("something else!"), } - Question 8Junior
What is the Option type in Rust, and why is it useful?
- Rust
Reveal answerHide answer
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.
- Question 9Junior
What is Rust?
- Rust
Reveal answerHide answer
Rust is a systems programming language that focuses on safety, concurrency, and performance. It was developed by Mozilla Research and first appeared in 2010. Rust aims to address common pitfalls in programming, such as memory safety issues, data races, and undefined behavior, often found in languages like C and C++. Rust is a statically compiled language with a rich type system and ownership model. It can be used to power performance-critical services while guaranteeing memory safety and thread safety, empowering developers to debug at compile-time.
- Question 11Junior
What is a Lifetime in Rust?
- Rust
Reveal answerHide answer
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: lifetimes connect data to the stack frame on which their owner is placed; once that stack frame exits, the owner goes out of scope and any pointers to/into that value/data structure are no longer valid, and the lifetime is a way for the compiler to reason about this. when an instance is borrowed (a reference is taken), it is borrowed for a specific lifetime. The borrow checker will try to minimize the lifetime of the borrow so that it can be as permissive as possible whilst ensuring the code is still safe. 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.
- Question 12Junior
Explain what is the relationship between Lifetimes andBorrow Checkers in Rust?
- Rust
Reveal answerHide answer
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.
Mid questions
16 questions- Question 1Mid
Explain what is Trait in Rust? What are some use cases when you need to implement one?
- Rust
Reveal answerHide answer
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: Code Reusability. Reduces duplication by providing a common interface for disparate types. Polymorphism and Dynamic Dispatch. Functions can accept generic types bounded by traits, allowing them to operate on different concrete types at runtime through dynamic dispatch. Abstracting Behavior. To abstract behavior from concrete implementations without changing the code that uses those behaviors. Enabling Generics. To provide a common interface for generic functions. Extension Methods. To add methods to types without modifying the original type.
Ad-Hoc Polymorphism. To achieve ad-hoc polymorphism, where different types can have the same name for functions, enabling you to write more generic code.
- Question 2Mid
Is it possible to create a Rust function with default arguments?
- Rust
Reveal answerHide answer
Problem
For example:
fn add(a: int = 1, b: int = 2) { a + b } ### SolutionNo, Rust doesn't support default function arguments. You have to define different methods with different names or use Option workaround:
fn add(a: Option<i32>, b: Option<i32>) -> i32 { a.unwrap_or(1) + b.unwrap_or(2) } The function call will look like add(None, None) . - Question 2Mid
When can’t my type be Copy in Rust?
- Rust
Reveal answerHide answer
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:: bytes. If you try to implement Copy on a struct or enum containing non-Copy data, you will get the error E0204 .
- Question 3Mid
Is String type in Rust Copy or Clone ?
- Rust
Reveal answerHide answer
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 .
- Question 4Mid
Does Rust support OOP?
- Rust
Reveal answerHide answer
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. Rust's struct type can be used to define data structures, similar to classes in traditional OOP languages. Trait s in Rust are akin to interfaces in other languages. They define a set of methods that types can implement to provide a common behavior. Rust doesn't support classical inheritance like some other languages. Instead, you can use composition to achieve similar goals. 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()); } - Question 5Mid
What does Rust have instead of a Garbage Collector?
- Rust
Reveal answerHide answer
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 (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 this means:
- Releasing the memory buffer associated to each String
- Releasing the memory buffer associated to the Vec itself Thus, 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).
- Question 6Mid
Why are Rust executables so huge? How would you optimise it?
- Rust
Reveal answerHide answer
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:
- Use Rust 1.32.0 or newer (which doesn't include jemalloc by default)
- Add the following to 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 = true requires Rust 1.59+. On older Rust versions, run strip manually on the resulting binary. 3. Build in release mode using cargo build --release 4. To force Rust to dynamically link programs, use the command-line arguments -C prefer-dynamic ; this willresult in a much smaller file size but will also require the Rust libraries (including its runtime) to be available to your program at runtime. 5. On Linux, at least, you can also strip the binary of symbols using the strip command: strip target/release/ The stat: The default release version of hello world (linux x86_64). 3.5 M, with prefer-dynamic 8904 B, stripped 6392 B.
- Question 7Mid
What is the difference between iter and into_iter in Rust?
- Rust
Reveal answerHide answer
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 youwant 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 theelements of my_vec into the ... scope. If you just need to look at the data, use iter , if you need to edit/mutate it, use iter_mut , and if you need to give it a new owner, use into_iter .
- Question 8Mid
Explain the concept of Ownership in Rust. Why do we need it in Rust?
- Rust
Reveal answerHide answer
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 isdropped 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 } - Question 13Mid
Compare Rc<T> vs Arc<T> in Rust
- Rust
Reveal answerHide answer
By 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 and Arc (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: They each implement Clone and Drop : cloning increments the reference count while dropping one decrements it Both Rc and Arc only allow you to get immutable references to the underlying data Arc is thread-safe, Rc isn't Arc uses atomic operations to manage the reference count, which gives it a higher runtime cost but makes it safe to share between threads Rc is the faster alternative if you’re only working in a single thread
- Question 14Mid
How would you model a Many-to-Many relationship with a Reference-Counted Smart Pointer in Rust?
- Rust
Reveal answerHide answer
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()], } } - Question 15Mid
Give an example of when, and how, you use an associated type in a custom trait in Rust
- Rust
Reveal answerHide answer
Associated types connect a type placeholder with a trait such that the trait method definitions can use these placeholder types in their signatures. The implementor of a trait will specify the concrete type to be used instead of the placeholder type for the particular implementation. That way, we can define a trait that uses some types without needing to know exactly what those types are until the trait is implemented. Let's consider an example of a simple trait that represents a collection type. We'll use an associated type to represent the type of elements stored in the collection.
trait Collection { type Item; // Associated type representing the type of elements in the collection fn add(&mut self, item: Self::Item); fn get(&self, index: usize) -> Option<&Self::Item>; fn size(&self) -> usize; } struct VecCollection<T> { data: Vec<T>, } impl<T> Collection for VecCollection<T> { type Item = T; // Associate the associated type with the concrete type fn add(&mut self, item: Self::Item) { self.data.push(item); } fn get(&self, index: usize) -> Option<&Self::Item> { self.data.get(index) } fn size(&self) -> usize { self.data.len() } }Here's how you might use the VecCollection implementation:
fn main() { let mut collection = VecCollection { data: Vec::new() }; collection.add(42); collection.add(55); println!("Size: {}", collection.size()); if let Some(item) = collection.get(1) { println!("Item at index 1: {}", item); } } - Question 16Mid
Define an extension trait to format any iterable of anydisplayable types in a custom format in Rust
- Rust
Reveal answerHide answer
In 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); } - Question 17Mid
Count word occurrences in a text file, print the top 10 from most to least common in Rust
- Rust
Reveal answerHide answer
This program reads the file line by line, splits each line into words, and counts the occurrences of each word using a HashMap . Then it sorts the word counts in descending order and prints the top 10 most common words along with their counts.
use std::collections::HashMap; use std::fs::File; use std::io::{BufRead, BufReader}; fn main() -> Result<(), std::io::Error> { // Open the text file let file = File::open("your_file.txt")?; let reader = BufReader::new(file);// Initialize a HashMap to store word counts let mut word_count: HashMap<String, u32> = HashMap::new(); // Process each line in the file for line in reader.lines() { if let Ok(line) = line { // Split the line into words let words = line.split_whitespace(); // Count occurrences of each word for word in words { *word_count.entry(word.to_string()).or_insert(0) += 1; } } }// Sort and print the top 10 most common words let mut sorted_word_count: Vec<(&String, &u32)> = word_count.iter().collect(); sorted_word_count.sort_by(|a, b| b.1.cmp(a.1)); for (word, count) in sorted_word_count.iter().take(10) { println!("Word: {}, Count: {}", word, count); } Ok(()) } - Question 18Mid
What's the difference between self and Self in Rust?
- Rust
Reveal answerHide answer
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; } - Question 20Mid
What is the difference between Copy and Clone in Rust?
- Rust
Reveal answerHide answer
The 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 memcpy s, 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.
Senior questions
7 questions- Question 3Senior
Explain the use of Fn / FnMut / FnOnce family of traits in Rust
- Rust
Reveal answerHide answer
The Fn , FnMut , and FnOnce traits are used as trait bounds for generic functions that accept closures. By specifying these traits as bounds, you can write more flexible code that can work with different types of closures:
- Fn: The Fn trait represents closures that capture variables by reference (immutable borrow). This means that the closures can only read the captured variables but cannot modify them. Closures that implement Fn can be called immutably.
- FnMut: The FnMut trait represents closures that capture variables by mutable reference (mutable borrow). This means that the closures can read and modify the captured variables. Closures that implement FnMut can be called mutably.
- FnOnce: The FnOnce trait represents closures that consume (move) the captured variables. This means that the closures take ownership of the captured variables and can only be called once. After the closure is called, it cannot be called again because it has consumed its environment. A closure |...| ... will automatically implement as many of those as it can. Consider:
// A generic function that takes a closure and calls it with the given argument. fn call_closure<F>(closure: F, arg: i32) where F: Fn(i32) { closure(arg); } fn main() { let x = 10; let y = 20;// A closure that captures `x` by reference (immutable borrow) and prints the result. let add_closure = |num| println!("Addition: {}", num + x);// A closure that captures `y` by mutable reference (mutable borrow) and prints the result. let subtract_closure = |num| { println!("Subtraction: {}", y - num); y -= num; };// A closure that consumes `x` and prints the result. let multiply_closure = move |num| println!("Multiplication: {}", num * x); call_closure(add_closure, 5); call_closure(subtract_closure, 7); call_closure(multiply_closure, 3);// The following line won't compile because `multiply_closure` has moved `x`. // call_closure(multiply_closure, 3); }Here each closure captures variables using different strategies. The call_closure function takes a closure and an argument, where the closure must implement the Fn(i32) trait:
- add_closure captures x by reference, so it implements Fn(i32) .
- subtract_closure captures y by mutable reference, so it implements FnMut(i32) .
- multiply_closure consumes x , so it implements FnOnce(i32) .
- Question 4Senior
How do I create a global mutable singleton in Rust?
- Rust
Reveal answerHide answer
Avoid the global state in general. Instead, construct the object somewhere early (perhaps in main ), then pass mutable references to that object into the places that need it. Starting with Rust 1.63, it can be easier to work with global mutable singletons. Now that Mutex::new is const , you can use global static Mutex locks without needing lazy initialization:
use std::sync::Mutex; static GLOBAL_DATA: Mutex<Vec<i32>> = Mutex::new(Vec::new()); fn main() { GLOBAL_DATA.lock().unwrap().push(42); println!("{:?}", GLOBAL_DATA.lock().unwrap()); }Note that this also depends on the fact that Vec::new is const . If you need to use non- const functions to set up your singleton, you could wrap your data in an Option , and initially set it to None . This lets you use data structures like Hashset which currently cannot be used in a const context:
use std::sync::Mutex; use std::collections::HashSet; static GLOBAL_DATA: Mutex<Option<HashSet<i32>>> = Mutex::new(None); fn main() { *GLOBAL_DATA.lock().unwrap() = Some(HashSet::from([42])); println!("V2: {:?}", GLOBAL_DATA.lock().unwrap()); }Alternatively, you could use an RwLock, instead of a Mutex , since RwLock::new is also const as of Rust 1.63. This would make it possible to read the data from multiple threads simultaneously.
- Question 9Senior
How do I create a global mutable singleton in Rust?
- Rust
Reveal answerHide answer
Avoid the global state in general. Instead, construct the object somewhere early (perhaps in main ), then pass mutable references to that object into the places that need it. Starting with Rust 1.63, it can be easier to work with global mutable singletons. Now that Mutex::new is const , you can use global static Mutex locks without needing lazy initialization:
use std::sync::Mutex; static GLOBAL_DATA: Mutex<Vec<i32>> = Mutex::new(Vec::new()); fn main() { GLOBAL_DATA.lock().unwrap().push(42); println!("{:?}", GLOBAL_DATA.lock().unwrap()); }Note that this also depends on the fact that Vec::new is const . If you need to use non- const functions to set up your singleton, you could wrap your data in an Option , and initially set it to None . This lets you use data structures like Hashset which currently cannot be used in a const context:
use std::sync::Mutex; use std::collections::HashSet; static GLOBAL_DATA: Mutex<Option<HashSet<i32>>> = Mutex::new(None); fn main() { *GLOBAL_DATA.lock().unwrap() = Some(HashSet::from([42])); println!("V2: {:?}", GLOBAL_DATA.lock().unwrap()); }Alternatively, you could use an RwLock, instead of a Mutex , since RwLock::new is also const as of Rust 1.63. This would make it possible to read the data from multiple threads simultaneously.
- Question 10Senior
Explain the use of Fn / FnMut / FnOnce family of traits in Rust
- Rust
Reveal answerHide answer
The Fn , FnMut , and FnOnce traits are used as trait bounds for generic functions that accept closures. By specifying these traits as bounds, you can write more flexible code that can work with different types of closures:
- Fn: The Fn trait represents closures that capture variables by reference (immutable borrow). This means that the closures can only read the captured variables but cannot modify them. Closures that implement Fn can be called immutably.
- FnMut: The FnMut trait represents closures that capture variables by mutable reference (mutable borrow). This means that the closures can read and modify the captured variables. Closures that implement FnMut can be called mutably.
- FnOnce: The FnOnce trait represents closures that consume (move) the captured variables. This means that the closures take ownership of the captured variables and can only be called once. After the closure is called, it cannot be called again because it has consumed its environment. A closure |...| ... will automatically implement as many of those as it can. Consider:
// A generic function that takes a closure and calls it with the given argument. fn call_closure<F>(closure: F, arg: i32) where F: Fn(i32) { closure(arg); } fn main() { let x = 10; let y = 20;// A closure that captures `x` by reference (immutable borrow) and prints the result. let add_closure = |num| println!("Addition: {}", num + x);// A closure that captures `y` by mutable reference (mutable borrow) and prints the result. let subtract_closure = |num| { println!("Subtraction: {}", y - num); y -= num; };// A closure that consumes `x` and prints the result. let multiply_closure = move |num| println!("Multiplication: {}", num * x); call_closure(add_closure, 5); call_closure(subtract_closure, 7); call_closure(multiply_closure, 3);// The following line won't compile because `multiply_closure` has moved `x`. // call_closure(multiply_closure, 3); }Here each closure captures variables using different strategies. The call_closure function takes a closure and an argument, where the closure must implement the Fn(i32) trait:
-
add_closure captures x by reference, so it implements Fn(i32) .
-
subtract_closure captures y by mutable reference, so it implements FnMut(i32) .
-
multiply_closure consumes x , so it implements FnOnce(i32) .
- Question 11Senior
Explain what is Send and Sync in Rust and when do you need them?
- Rust
Reveal answerHide answer
In Rust, Send and Sync are marker traits that are important when dealing with multi-threaded code to ensure safe sharing and transfer of data between threads. The Send trait is used to indicate that a type can be safely transferred across thread boundaries. When a type implements Send , it means it is safe to move or send instances of that type to a different thread. The type guarantees that it doesn't have any interior mutability or shared references, which could lead to data races when accessed from multiple threads. The Sync trait indicates that a type can be safely shared between multiple threads. When a type implements Sync , it means it is safe to have multiple threads simultaneously read from that type without causing data races. Types that are immutable or have interior mutability (e.g., Mutex , RwLock ) and ensure proper synchronization are typically Sync . You should only implement Send and Sync when you are sure that the type's behavior is truly safe for concurrent use. use std::thread;// A type that implements
Sendand can be safely transferred between threads. struct SendExample;// A type that implementsSyncand can be safely shared between threads.struct SyncExample { data: u32, }// Implement `Send` for `SendExample`. unsafe impl Send for SendExample {}// Implement `Sync` for `SyncExample`. unsafe impl Sync for SyncExample {} fn main() { // Create a SendExample instance and move it to a new thread. let send_example = SendExample; let send_handle = thread::spawn(move || { println!("This is a SendExample thread."); });// Wait for the thread to finish. send_handle.join().unwrap();// Create a SyncExample instance and share it between multiple threads. let sync_example = SyncExample { data: 42 }; let sync_handle1 = thread::spawn(move || { println!("Thread 1: Data is {}", sync_example.data); }); let sync_handle2 = thread::spawn(move || { println!("Thread 2: Data is {}", sync_example.data); });// Wait for both threads to finish. sync_handle1.join().unwrap(); sync_handle2.join().unwrap(); } - Question 12Senior
Is it possible to use global variables in Rust?
- Rust
Reveal answerHide answer
Declaring and using global variables in Rust can be tricky: The most common use cases for global state are not variables but constants. In Rust, they come in two flavors: Constant values, defined with const . These are inlined by the compiler. Interior mutability is never allowed. Static variables, defined with static . They receive a fixed space in the data segment. Interior mutability is possible. For mutability, you could use something like:
static mut N: i32 = 5; unsafe { N += 1; println!("N: {}", N); }- You can use static variables fairly easily as long as they are thread-local.
extern mod sqlite; use std::cell::RefCell; thread_local!(static ODB: RefCell<sqlite::database::Database> = RefCell::new(sqlite::open("test.db")); fn main() { ODB.with(|odb_cell| { let odb = odb_cell.borrow_mut(); // code that uses odb goes here }); } - Question 15Senior
Explain the use of async / .await in Rust
- Rust
Reveal answerHide answer
Use async in front of fn , closure , or a block to turn the marked code into a Future . As such the code will not be run immediately, but will only be evaluated when the returned future is .await ed.
async fn learn_song() -> Song { /* ... */ } async fn sing_song(song: Song) { /* ... */ } async fn dance() { /* ... */ }The value returned by async fn is a Future . For anything to happen, the Future needs to be run on an executor.
// `block_on` blocks the current thread until the provided future has run to // completion fn main() { let song = block_on(learn_song()); block_on(sing_song(song)); block_on(dance()); }Unlike block_on , .await doesn't block the current thread, but instead asynchronously waits for the future to complete, allowing other tasks to run if the future is currently unable to make progress.
async fn learn_and_sing() { // Wait until the song has been learned before singing it. // We use `.await` here rather than `block_on` to prevent blocking the // thread, which makes it possible to `dance` at the same time. let song = learn_song().await; sing_song(song).await; } async fn async_main() { let f1 = learn_and_sing(); let f2 = dance();// `join!` is like `.await` but can wait for multiple futures concurrently. // If we're temporarily blocked in the `learn_and_sing` future, the `dance` // future will take over the current thread. If `dance` becomes blocked, // `learn_and_sing` can take back over. If both futures are blocked, then // `async_main` is blocked and will yield to the executor. futures::join!(f1, f2); } fn main() { block_on(async_main()); }
Expert questions
3 questions- Question 5Expert
What are the specific conditions for a closure to implement the Fn , FnMut and FnOnce traits?
- Rust
Reveal answerHide answer
Problem
Explain:
When does a closure not implement the FnOnce trait? When does a closure not implement the FnMut trait? When does a closure not implement the Fn trait? For instance, mutating the state of the closure on it's body makes the compiler not implement Fn on it.
Solution
The traits each represent more and more restrictive properties about closures/functions, indicated by the signatures of their call_... method, and particularly the type of self :
FnOnce ( self ) are functions that can be called once FnMut ( &mut self ) are functions that can be called if they have &mut access to their environment Fn ( &self ) are functions that can be called if they only have & access to their environmentA closure |...| ... will automatically implement as many of those as it can. All closures implement FnOnce : a closure that can't be called once doesn't deserve the name. Note that if a closure only implements FnOnce , it can be called only once. Closures that don't move out of their captures implement FnMut , allowing them to be called more than once (if there is unaliased access to the function object). Closures that don't need unique/mutable access to their captures implement Fn , allowing them to be called essentially everywhere.
- Question 6Expert
How to implement a custom Allocator in Rust?
- Rust
Reveal answerHide answer
Problem
You are looking for a way to implement something like a memory pool in Rust. You want to allocate a set of related small objects in chunks and delete the set of objects at once. The objects won't be freed separately. There are several benefits to this approach: It reduces fragmentation. It saves memory. Is there any way to create an allocator like this in Rust?
Solution
In Rust, you can implement a custom allocator by defining a type that implements the GlobalAlloc trait. This trait is provided by the standard library and is used by Rust's memory allocation routines. Implementing a custom allocator allows you to control how memory is allocated and deallocated in your program.
use std::alloc::{GlobalAlloc, Layout}; use std::ptr::{null_mut, NonNull}; use std::sync::Mutex; const CHUNK_SIZE: usize = 4096; // Example chunk size struct Chunk { data: [u8; CHUNK_SIZE], used: usize, } struct ChunkAllocator { chunks: Mutex<Vec<Chunk>>, } unsafe impl GlobalAlloc for ChunkAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { let mut chunks = self.chunks.lock().unwrap(); let mut last_chunk = chunks.last_mut(); if let Some(chunk) = last_chunk { if chunk.used + layout.size() <= CHUNK_SIZE { let ptr = chunk.data.as_mut_ptr().add(chunk.used); chunk.used += layout.size(); return ptr; } } let new_chunk = Chunk { data: [0u8; CHUNK_SIZE], used: layout.size(), }; let ptr = new_chunk.data.as_mut_ptr(); chunks.push(new_chunk); ptr } unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { // Deallocating in chunks, not individually // No need to implement deallocation logic here } } #[global_allocator] static GLOBAL_ALLOCATOR: ChunkAllocator = ChunkAllocator { chunks: Mutex::new(vec![]), }; fn main() { let data1 = Box::new([1, 2, 3, 4, 5]); let data2 = Box::new([10, 20, 30, 40, 50]); // More allocations... // Objects are allocated in chunks and deallocated together }- Chunk represents a single chunk of memory that can hold multiple objects.
- ChunkAllocator manages a collection of chunks and implements the GlobalAlloc trait.
- The alloc method attempts to allocate memory within the last chunk. If there's not enough space, a new chunk is created and added to the collection.
- The dealloc method doesn't need to do anything since deallocation is done in chunks when the allocator itself is dropped.
- The GLOBAL_ALLOCATOR instance of ChunkAllocator is used as the global allocator throughout the program, ensuring that allocations are managed as desired.
- Question 17Expert
When should I use Box , Arc , Rc , Cell , RefCell , RwLock and Mutex ?
- Rust
Reveal answerHide answer
You use Box when you want to opt-in to storing something on the heap, which otherwise wouldn't go there. You should do this when: i. You are writing a recursive type. ii. You want to use dyn SomeTrait , and you want ownership of it. iii. You are actively optimizing code, and you have a really large struct that is expensive to move around. You use Arc and Rc when you want to have shared ownership to some value. This means that you want to access the same variable from many different places, and any one of them should keep the value alive. The only distinction between Arc and Rc is that the former is very slightly more expensive, but the latter is not thread-safe. Anything shared with Rc / Arc is immutable. You use Cell , RefCell or Mutex when you have something that is shared, and you want mutable access to it. The two cell types are not thread-safe, so if you are doing anything multi-threaded, you should use a mutex. As for choosing between Cell and RefCell , you should pick a Cell when the type is Copy , and RefCell otherwise. You should use RwLock over Mutex if you know you're going to do reads far more frequently than writes So when you see Rc<RefCell<...>> or Arc<Mutex<...>> , this is because people have a value they want to share, but they also want to mutate it. The Rc / Arc allows them to share the value, and the RefCell / Mutex allows them to mutate it.
TL;DR;- Explicitly store on the heap: Box
- Shared ownership: Arc and Rc
- Mutate something shared: Cell , RefCell , Mutex