Skip to content

Instantly share code, notes, and snippets.

@erhangundogan
Last active April 13, 2024 16:35
Show Gist options
  • Save erhangundogan/57e3b9a4b3f19ce3e8e46784f5bf8224 to your computer and use it in GitHub Desktop.
Save erhangundogan/57e3b9a4b3f19ce3e8e46784f5bf8224 to your computer and use it in GitHub Desktop.

Rust Tips & Tricks

Ownership

https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html

If variable is defined on stack then it won't move:

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y); // works: x = 5, y = 5
let s1 = "hello";
let _s2 = s1;

println!("{}, world!", s1); // works: hello world!

If variable is defined on heap then it will move:

let s1 = String::from("hello");
let _s2 = s1; // s1 moves into s2 and s1 dropped

println!("{}, world!", s1); // error

References and Borrowing

If we pass address (reference or pointer) of the variable then it won't be dropped

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len); // works: The length of 'hello' is 5.
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Mutable References

Just as variables are immutable by default, so are references. We’re not allowed to modify something we have a reference to. We should define mutable reference to change it.

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value. This code that attempts to create two mutable references to s will fail:

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2); // error

That could be resolve with scopes:

let mut s = String::from("hello");

{
    let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.

let r2 = &mut s;

Enums

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

if let

You can think of if let as syntax sugar for a match that runs code when the value matches one pattern and then ignores all other values.

So this block of code:

if let x = y {
   foo();
} else {
   bar();
}

Is equivalent to using a full match:

match y {
    x => {
        foo();
    }
    _ => {
        bar();
    }
}

Using an Enum to store multiple types

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

Error handling

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error);
            }
        },
    };
}

Alternatives to Using match with Result<T, E>

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

Using match works well enough, but it can be a bit verbose and doesn’t always communicate intent well. The Result<T, E> type has many helper methods defined on it to do various, more specific tasks. The unwrap method is a shortcut method implemented just like the match expression. If the Result value is the Ok variant, unwrap will return the value inside the Ok. If the Result is the Err variant, unwrap will call the panic! macro for us. Here is an example of unwrap in action:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

If we run this code without a hello.txt file, we’ll see an error message from the panic!

Similarly, the expect method lets us also choose the panic! error message. Using expect instead of unwrap and providing good error messages can convey your intent and make tracking down the source of a panic easier. The syntax of expect looks like this:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

Propagating errors

When a function’s implementation calls something that might fail, instead of handling the error within the function itself, you can return the error to the calling code so that it can decide what to do. This is known as propagating the error and gives more control to the calling code, where there might be more information or logic that dictates how the error should be handled than what you have available in the context of your code.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

A Shortcut for Propagating Errors: the ? Operator

Function below has the same functionality as previous one, but this implementation uses the ? operator.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

The ? placed after a Result value is defined to work in almost the same way as the match expressions we defined to handle the Result values in previous function. If the value of the Result is an Ok, the value inside the Ok will get returned from this expression, and the program will continue. If the value is an Err, the Err will be returned from the whole function as if we had used the return keyword so the error value gets propagated to the calling code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment