Rust Coding Questions and Answers
Welcome to this curated collection of Rust coding questions and answers. As your friendly librarian, I have gathered some of the most important concepts to help you master Rust!
You'll find questions separated into:
- Basic Concepts
- Advanced Concepts
Q: What is the difference between String and &str in Rust?
Answer:
In Rust, String and &str are both used to handle string data, but they have distinct differences in how they manage memory and handle ownership:
-
String:- It is an owned type.
- It is stored on the heap, meaning its size can grow or shrink at runtime.
- When a
Stringgoes out of scope, its memory is automatically freed (dropped). - You can mutate it (if it's declared
mut), e.g., by pushing new characters or strings to it.
-
&str(String Slice):- It is a borrowed type (a reference).
- It represents a view into a block of memory that contains a string (which could be on the heap, stack, or hardcoded in the binary as a static string).
- It does not have ownership of the data it points to; it merely looks at it temporarily.
- It is immutable by default and its size is fixed.
Example:
fn main() { // A String (heap-allocated, owned, growable) let mut my_string = String::from("Hello"); my_string.push_str(", world!"); // A &str (string slice, borrowed, fixed-size view) let my_slice: &str = &my_string[0..5]; // Borrows "Hello" // Hardcoded string literals are also of type &str (specifically &'static str) let static_str: &str = "I am stored in the binary"; }
Q: Explain Ownership and Borrowing in Rust.
Answer:
Ownership is Rust's most unique feature, which guarantees memory safety without needing a garbage collector. It operates on three main rules:
- Each value in Rust has a single variable that is its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (its memory is freed).
Borrowing is how Rust allows you to access a value without taking ownership of it, using references (& or &mut). It solves the problem of needing to pass values to functions without losing ownership.
Borrowing has two strict rules:
- At any given time, you can have either one mutable reference (
&mut T) or any number of immutable references (&T). - References must always be valid (Rust prevents dangling pointers).
Example of Ownership:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; // Ownership moves to s2 // println!("{}", s1); // Error! s1 is no longer valid }
Example of Borrowing:
#![allow(unused)] fn main() { fn calculate_length(s: &String) -> usize { // Takes an immutable reference s.len() } // s goes out of scope, but since it doesn't have ownership, nothing is dropped. let s1 = String::from("hello"); let len = calculate_length(&s1); // We borrow s1 instead of moving it }
Q: How do you handle errors in Rust?
Answer:
Rust groups errors into two major categories: Recoverable and Unrecoverable errors.
1. Recoverable Errors (Result<T, E>)
For errors that can be handled gracefully (like a file not being found), Rust uses the Result enum:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
You can handle Result using match or the ? operator.
Using match:
#![allow(unused)] fn main() { use std::fs::File; let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => panic!("Problem opening the file: {:?}", error), }; }
Using the ? Operator:
The ? operator is a shorthand that unwraps Ok values or immediately returns the Err from the current function.
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } }
2. Unrecoverable Errors (panic!)
For situations where the program reaches a state that it cannot recover from (like accessing an array out of bounds), Rust provides the panic! macro. When a panic occurs, the program will print a failure message, unwind and clean up the stack, and then quit.
fn main() { panic!("crash and burn"); }
Q: How does Pattern Matching work in Rust?
Answer:
Pattern matching in Rust is extremely powerful and allows you to compare a value against a series of patterns and execute code based on which pattern matches. This is primarily done using the match and if let constructs.
1. The match Operator
match takes a value and routes control to branches (arms) based on matching patterns. It is exhaustive, meaning every possible case must be covered.
#![allow(unused)] fn main() { enum Coin { Penny, Nickel, Dime, Quarter(String), // Can hold data } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {}!", state); 25 } } } }
2. if let Syntax
When you only care about matching one specific pattern while ignoring the rest, match can be overly verbose. In these cases, you can use if let.
#![allow(unused)] fn main() { let some_u8_value = Some(0u8); // Using match: match some_u8_value { Some(3) => println!("three"), _ => (), // Does nothing for other values } // Using if let (more concise): if let Some(3) = some_u8_value { println!("three"); } }