Rust Practice Drills
Practice problems generated to reinforce Rust syntax after learning the basics. For fundamentals on variables, ownership, and borrowing, see Learning Rust Basics.
Structs and Implementation
Define data structures and methods (Rust’s class equivalent):
struct User {
username: String,
email: String,
login_count: u64,
}
impl User {
fn new(username: &str, email: &str) -> Self {
Self {
username: username.to_string(),
email: email.to_string(),
login_count: 0,
}
}
fn display(&self) {
println!("User: {} <{}>", self.username, self.email);
}
fn increment_login(&mut self) {
self.login_count += 1;
}
}Enums and Match
Rust enums can carry associated data. Pattern matching with match destructures
the variants:
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
fn to_celsius(temp: Temperature) -> f64 {
match temp {
Temperature::Celsius(c) => c,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}Ownership and Borrowing in Practice
Borrow (&) for read access, move for ownership transfer. Attempting to use
a moved value causes a compile error:
fn print_list(val: &Vec<String>) {
for item in val {
println!("{item}");
}
}
fn consume_list(val: Vec<String>) {
println!("Length: {}", val.len());
} // val is dropped here
fn main() {
let list = vec!["a".into(), "b".into()];
print_list(&list); // borrow
consume_list(list); // move
// print_list(&list); // compile error: value moved
}Result and Error Handling
Functions that can fail return Result<T, E>. Handle the result with match
or if let:
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Cannot divide by zero".to_string())
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("{result}"),
Err(e) => println!("Error: {e}"),
}
if let Err(e) = divide(10.0, 0.0) {
println!("Error: {e}");
}
}External Crates
Add dependencies in Cargo.toml and use them:
[dependencies]
rand = "0.8"use rand::Rng;
fn random_index(vec: &[String]) -> Option<&String> {
if vec.is_empty() {
None
} else {
let idx = rand::thread_rng().gen_range(0..vec.len());
Some(&vec[idx])
}
}Traits
Traits define shared behaviour (similar to interfaces in other languages):
trait Cipher {
fn encrypt(&self, data: &str) -> String;
}
struct CaesarShift;
impl Cipher for CaesarShift {
fn encrypt(&self, data: &str) -> String {
data.chars().map(|c| (c as u8 + 3) as char).collect()
}
}
fn use_cipher(cipher: &dyn Cipher, data: &str) -> String {
cipher.encrypt(data)
}Generics
Generic types allow type-safe abstraction over multiple types:
struct Vault<T> {
value: T,
}
impl<T> Vault<T> {
fn new(value: T) -> Self {
Self { value }
}
fn get(&self) -> &T {
&self.value
}
}
// Instantiation infers the type
let v = Vault::new(String::from("secret"));
let w = Vault::new(42u32);Dynamic Dispatch
Use Box<dyn Trait> to store heterogeneous types in a collection:
trait Plugin {
fn name(&self) -> &str;
fn run(&self);
}
struct AudioPlugin;
impl Plugin for AudioPlugin {
fn name(&self) -> &str { "Audio" }
fn run(&self) { println!("Playing sound..."); }
}
struct VideoPlugin;
impl Plugin for VideoPlugin {
fn name(&self) -> &str { "Video" }
fn run(&self) { println!("Showing frames..."); }
}
fn main() {
let plugins: Vec<Box<dyn Plugin>> = vec![
Box::new(AudioPlugin),
Box::new(VideoPlugin),
];
for plugin in plugins {
plugin.run();
}
}