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();
    }
}

References