10 Mar 2023
There’s no way to run away from errors, eventually they will happen. Usually there are two kinds of errors:
- Recoverable: Is an error where you can do something about it and you want to continue with the execution
- Unrecoverable: Is an error where you can’t do anything about it and the application just has to stop.
Rust has ways to deal with both.
panic!
at the program
First lets talk about unrecoverable errors. Your application might be in a state where you want to stop everything, for that you can use the panic!
macro. This will cause the application to stop and print the Backtrace (in Rust is called Backtrace, but you probably hear as stacktrace) showing the calls.
We have the following program that will panic:
fn main() {
panic!("Something wrong is not right");
}
When we try cargo run
that we get
Compiling panic v0.1.0 (rust-book/panic)
Finished dev [unoptimized + debuginfo] target(s) in 1.27s
Running `target/debug/panic`
thread 'main' panicked at 'Something wrong is not right', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
It shows the message that I passed to the panic!
macro and the location where it happens. Of course that the entire code won’t be at the main
function.
fn main() {
will_call_someting_that_panics();
}
fn will_call_someting_that_panics() {
will_panic();
}
fn will_panic() {
panic!("Something wrong is not right");
}
But we still get the same message when we run cargo run
:
Compiling panic v0.1.0 (rust-book/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.84s
Running `target/debug/panic`
thread 'main' panicked at 'Something wrong is not right', src/main.rs:10:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
So we add the RUST_BACKTRACE=1
that the note mentions and:
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/panic`
thread 'main' panicked at 'Something wrong is not right', src/main.rs:10:5
stack backtrace:
0: rust_begin_unwind
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
2: panic::will_panic
at ./src/main.rs:10:5
3: panic::will_call_someting_that_panics
at ./src/main.rs:6:5
4: panic::main
at ./src/main.rs:2:5
5: core::ops::function::FnOnce::call_once
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Now we know the path that was made to get in there, but the RUST_BACKTRACE
gives a way smaller message in case you are using a release build like RUST_BACKTRACE=1 cargo run --release
:
Compiling panic v0.1.0 (rust-book/panic)
Finished release [optimized] target(s) in 0.14s
Running `target/release/panic`
thread 'main' panicked at 'Something wrong is not right', src/main.rs:10:5
stack backtrace:
0: rust_begin_unwind
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
2: panic::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Unwinding and cleaning after yourself
After panic!
Rust will execute a unwind process, which will go back and free all the memory and resources taken. This is important to avoid memory leaks or cause issues with dealing with resources like files or sockets.
In the Rust Book they mention that you can remove the unwind process to make the application smaller by changing the Cargo.toml
.
[profile.release]
panic = 'abort'
Recoverable Errors
You don’t have to panic!
about everything, some errors are recoverable. The file you read isn’t there? the api call failed? That’s all fine and you can move on with life using the Result<T, E>
construct. Very much like the Option<T>
, this is an enum where a possible failure is wrapped. The declaration of Result<T, E>
is:
enum Result<T, E> {
Ok(T),
Err(E),
}
In this case there’s is the generic type E
so we can also pass the error. So when you try to read a file you can do:
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
match greeting_file_result {
Ok(_) => println!("File was read"),
Err(error) => println!("There was an error reading file: {:?}", error),
}
println!("But there's not reason to panic! about")
}
Since the file does not exists you will get
Finished dev [unoptimized + debuginfo] target(s) in 0.14s
Running `target/debug/panic`
There was an error reading file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
But there's not reason to panic! about
To narrow on the error type is also possible to check the kind()
function to get the specific error that it was returned.
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
match greeting_file_result {
Ok(_) => println!("File was read"),
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => println!("File not found"),
anything_else => panic!("Something is wrong {:?}", anything_else),
},
}
println!("But there's not reason to panic! about")
}
Syntactic Sugar
Now, you don’t have to use match
every time you deal with result. Instead there are a set of functions it can be used that will panic the application if that’s what you want.
unwrap_or_else
: it does sounds like a threat but this just adds a way to get the value or do something with the error
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);
}
});
}
unwrap
: It gives you the value or panic!
the application, makes it shorter when you want to panic!
on error.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
expect
: It’s just like unwrap
but you can pass a nice error message to it. Its usually preferred since it makes easier to understand what’s going on.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
Simplifying calls with ?
The Result<T, E>
enum is very useful to handle recoverable errors, and we have a lot of recoverable errors in the day to day. Think of the following example:
- Parse a string to json
- Validate the JSON shape
- Get a value from it
- make a database call
- add a value based on the json + db data into a queue.
Every single of those steps can have errors, so we would have to use result in them. The code for that is lengthy and repetitive, look on how do_work
ends becoming the same thing over and over:
enum PaymentType {
CASH,
CREDIT,
DEBIT,
}
struct Payment {
amount: isize,
payment_type: PaymentType,
user_id: usize,
}
struct PaymentError {
reason: String,
}
fn main() {
let valid_data = String::from("{}");
match do_work(valid_data) {
Ok(()) => println!("Success!"),
Err(err) => println!("Error: {err}"),
};
let invalid_data = String::from("invalid");
match do_work(invalid_data) {
Ok(()) => println!("Success!"),
Err(err) => println!("Error: {err}"),
};
}
fn parse_json(json: String) -> Result<Payment, String> {
if json.starts_with("{") {
return Ok(Payment {
amount: 150,
payment_type: PaymentType::CASH,
user_id: 42,
});
}
return Err(String::from("Could not parse json"));
}
fn get_user_email(user_id: usize) -> Result<String, String> {
if user_id == 42 {
return Ok(String::from("email@example.org"));
}
return Err(String::from("Could not find user email"));
}
fn send_confirmation_email(email: String) -> Result<(), String> {
println!("Will send email to {email}");
return Ok(());
}
fn do_work(data: String) -> Result<(), String> {
let payment = match parse_json(data) {
Ok(payment) => payment,
Err(err) => return Err(err),
};
let user_email = match get_user_email(payment.user_id) {
Ok(email) => email,
Err(error) => return Err(error),
};
match send_confirmation_email(user_email) {
Ok(()) => println!("Confirmation email sent"),
Err(err) => return Err(err),
};
return Ok(());
}
There is the ?
operator that allow us to chain those operations so we can work with the success values sequentially. Let’s refactor do_work
to use that
fn do_work(data: String) -> Result<(), String> {
let user_id = parse_json(data)?.user_id;
let user_email = get_user_email(user_id)?;
send_confirmation_email(user_email)?;
return Ok(());
}
Both versions of do_work
will have the same result but the second one is way easier to deal with it. The ?
operator propagates the Err
to the caller, so we don’t have to deal with that, it’s almost like a throw
but more behaved.
08 Mar 2023
Rust has two types of collections:
- Stored in the stack: Like array and tuples, you need to know the sizes of those collections so Rust can allocate the right amount of memory.
- The ones stored in the heap: The point of those are to be with dynamic length, so you don’t need to know their size beforehand. The main ones are
vector
, string
and hash map
.
Vector
The first one is the vector
, which is a dynamic length list, where you can add or remove values. To instantiate a new vector
you do:
// When using Vec::new() we need to declare the type
// because it is a generic function and
// the type can't be inferred automatically
let v: Vec<i32> = Vec::new();
// In case you already have an initial value you can use vec!
// which will create a vector with the values passed and it will
// also infer the type for you
let v = vec![1, 2, 3];
Even those dynamic data structures are immutable, so if you want to add items to it you need to declare them as mutable.
// This will throw an error
let immutable_vector: Vec<i32> = Vec::new();
immutable_vector.push(1); // error[E0596]: cannot borrow `immutable_vector` as mutable, as it is not declared as mutable
// This is allowed
let mut mutable_vector: Vec<i32> = Vec::new();
mutable_vector.push(1);
mutable_vector.push(2);
mutable_vector.push(3);
Memory safety
Now, if you are storing something in a vector, you probably want to read it. There are two ways of doing data.
The unsafe way using &vector[index]
that gives you a reference of the value:
let v: Vec<i32> = vec![1, 2, 3, 4, 5];
let id = &v[0];
println!("Something {}", id); // Something 1
let invalid_index = &v[10];
println!("Somethin else {}", invalid_index); // thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:3:15
There’s also the safe way with vector.get(index)
that will return an Option<&T>
.
let v: Vec<i32> = vec![1, 2, 3, 4, 5];
let id = v.get(1);
match id {
Some(id) => println!("The selected element is {}", id),
None => println!("No valid element was selected"),
} // The selected element is 2
let invalid_element = v.get(20);
match invalid_element {
Some(invalid_element) => println!("The selected element is {}", invalid_element),
None => println!("No valid element was selected"),
} // No valid element was selected
Now on Vectors and Safety, when you get a value from the value from a vector, you get all the borrow checks and memory ownership safeguard that rust has.
When you have the ownership of a value from the vector you can’t modify the same.
fn main() {
let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];
let value = &v[1]; // &v[1] or v.get(1) will enforce the rules
v.push(6);
println!("The element at 1 is {}", value);
}
When you try to compile, you will receive:
23 | let value = &v[1];
| - immutable borrow occurs here
24 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
25 | println!("The element at 1 is {}", value);
| ----- immutable borrow later used here
Also when a vector is dropped from scope the values are also dropped. The borrow checker will make sure that no one is using a value.
Finally you can easily iterate over vectors too
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
String
Rust has two types of string types. The &str
and the String
, the first one is stored in the stack and have fixed size. The other one is a dynamic one, where you can manipulate it. Here I will only talk about the second one.
We create those strings doing the following
let empty_string = String::new()
let test_string = String::from("Test");
let from_string = "from string".to_string();
You can update String
s if they are mutable
let mut s1 = String::from("foo");
let s2 = "bar";
s.push_str("_");
s.push_str(s2);
s.push('!'); // String.push() accepts a character
println!("s1 is {s1}"); // s1 is foo_bar
You can combine strings using +
, but you need to understand how the ownership will affect the variables.
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
println!("s1 is {s1}, s2 is {s2}, Value of s3: {s3}");
This will throw the following error during compilation
error[E0382]: borrow of moved value: `s1`
--> src\main.rs:5:22
|
2 | let s1 = String::from("Hello, ");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = String::from("world!");
4 | let s3 = s1 + &s2;
| -- value moved here
5 | println!("s1 is {s1}, s2 is {s2}, Value of s3: {s3}");
| ^^ value borrowed here after move
|
You can’t have &str + &str
because both are immutable. So in case you try to concatenate them
--> src\main.rs:4:18
|
4 | let s3 = &s1 + &s2;
| --- ^ --- &String
| | |
| | `+` cannot be used to concatenate two `&str` strings
| &String
|
= note: string concatenation requires an owned `String` on the left
help: remove the borrow to obtain an owned `String`
|
4 - let s3 = &s1 + &s2;
4 + let s3 = s1 + &s2;
|
So if you use the +
the ownership will be passed to the new variable. You can also use format!
to concatenate strings, with the advantage of not having to pass the ownership to the new value
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
println!("Value of s is {s}"); // Value of s is tic-tac-toe
println!("s1 is {s1}, s2 is {s2}, s3 is {s3}"); // s1 is tic, s2 is tac, s3 is toe
}
Slicing and Iterating
You can’t use []
to get a single character from a String but you can use to get a slice of a String with &var[0..10]
.
let text = "a string with some text";
let slice = &text[0..8];
println!("Sliced string is: {slice}"); // Sliced string is a string
You can also iterate other the over the string with .chars()
or .bytes()
let text = "a string with some text";
for c in text.chars() {
print!("{c}");
}
println!("");
for b in text.bytes() {
println!("{b}");
}
This will output to
a string with some text
9732115116114105110103321191051161043211511110910132116101120116
UTF-8 and why strings are not so simple.
Rust decided to favour safety over abstracting the complexity. Which makes quite different to work with strings in Rust, so I recommend reading
Storing UTF-8 Encoded Text with Strings - The Rust Programming Language
Hashmaps
HashMaps are to store data based on keys. This is how we can use the HashMap:
// You need to import from the collections
use std::collections::HashMap;
fn main() {
// You need to create a mutable HashMap if you
// want to add anything to it
let mut scores = HashMap::new();
// Inserting the values to the HashMap
// The key and value must be the same for all values
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// Iterating over the HashMap
for (key, value) in &scores {
println!("{key}: {value}");
}
}
You can easily retrieve the value from the HashMap:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// get - Retrieve the value from the HashMap, this will give you an Option<&V>
let blue_score = scores.get("Blue");
match blue_score {
Some(blue_score) => println!("Blue score is {blue_score}"),
None => println!("None"),
}
println!("Blue score is {blue_score}"); // Blue score is 10
}
There is also some convenient method for common operations
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// Combine entry and or_insert to insert a value if isn't in the HashMap
scores.entry(String::from("Red")).or_insert(30);
let red_score = scores.get("Red");
match red_score {
Some(red_score) => println!("Red score is {red_score}"),
None => println!("None"),
}
// You can also update the value in the same way
let green_score = scores.entry(String::from("Green")).or_insert(20);
*green_score += 5;
let green_score = scores.get("Green");
match green_score {
Some(green_score) => println!("Green score is {green_score}"),
None => println!("None"),
}
}
25 Feb 2023
In the Rust world they use the word crate
to identify a library that doesn’t have an entry point, and a binary to be something with a entry point.
Modules
Modules are the way that rust uses to organize and group code. You can declare modules using the mod
keyword. The way the code is structured in the folder is also the way where you will have your crates path, but isn’t simple like creating the folder and the files then importing it, you need to create a file with the module declaration.
You can also use modules as a way to group code and control what do you expose, you have the pub
modifier that makes the method public, since the default is to make everything private from it’s parents. Let’s use the restaurant example from the rust book we would need to create the following.
We have a main.rs
:
pub mod front_of_house;
fn main() {
crate::front_of_house::hosting::add_to_waitlist();
// Relative import is also allowed
front_of_house::hosting::add_to_waitlist();
}
// src/front_of_house.rs
pub mod hosting;
// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {
println!("Add to waitlist")
}
The file structure would be:
restaurant
├── Cargo.lock
├── Cargo.toml
└── src
├── front_of_house
│ └── hosting.rs
├── front_of_house.rs
└── main.rs
main.rs
has the declaration for the front_of_house
module,
front_of_house.rs
has the declaration for the hosting
module.
hosting.rs
has the function add_to_waitlist
.
Everything that has the pub
keyword inside the hosting.rs
file will be available to be used in the main.rs
through the crate::front_of_house::hosting::add_to_wallet()
.
The use
keyword
Specifying the full path every time you are going to call a function might not be the most ergonomic thing, to avoid that there’s the use
keyword that allows you to import things to the file.
We import the hosting
module to the src/main.rs
file.
pub mod front_of_house;
use front_of_house::hosting;
fn main() {
hosting::add_to_waitlist();
}
Conventions around use
Now rust has some conventions that were created with the time about how do you import things. In case of functions you should import the module and use the the module name and the function name in combination like we have in the example above. This way the reader knows that the function comes from another module.
Now for struct, enums and other values you should import the entire path. So if we add an enum to our hosting
module
// src/front_of_house/hosting.rs
#[derive(Debug)]
pub enum CustomerType {
REGULAR,
VIP,
}
pub fn add_to_waitlist(customer_type: CustomerType) {
println!("Add to waitlist {:?} customer", customer_type);
}
We can use the entire thing without having to do hosting::CustomerType
// src/main.rs
pub mod front_of_house;
use front_of_house::hosting;
use front_of_house::hosting::CustomerType;
fn main() {
hosting::add_to_waitlist(CustomerType::REGULAR);
}
The only exception for this rule is when you have two objects with the same name in a file, then you need to import the module to remove that ambiguity.
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
}
fn function2() -> io::Result<()> {
// --snip--
}
Another alternative is using the as
keyword that allow you to rename an import.
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
syntax sugar
Rust also has some unix like syntax sugar for importing so you don’t have to keep importing the same thing over and over.
The first one is using curly braces to import multiple things from the same path.
// This two imports
use std::cmp::Ordering;
use std::io;
// Can be turned into a single one
use std::{cmp::Ordering, io};
// You can use `self` to import std::io and a submodule
use std::io::{self, Write}
And there’s also a glob (*
) operator in case you want to import everything that is public for that path.
use std::collections::*;
fn main() {
let hash_map = HashMap::new()
}
When using the glob operator you might end up not knowing if is something in the scope or something that was defined by your program.
re-exporting with pub use
.
Finally, you have the reexport with the pub use
. This will make something that you imported available to the other packages. This is good when you want to export a different structure to the users.
mod front_of_house;
pub use front_of_house::hosting;
fn main() {
hosting::add_to_waitlist();
}
Using cargo
Cargo is rust build system and package manager, it’s a very good tool but it has some quirks.
Multiple entry points
Sometimes you want to have different entry points for your application. I was doing the Advent of Code and wanted to have everything into a single project but run them in isolation.
Multiple bins with a bin folder
Cargo can compile and run multiple executables if you have them under the src/bin
folder. So in the case were we have a code challenge every day, we can have a structure like
advent_of_code
├── Cargo.lock
├── Cargo.toml
└── src
├── bin
| ├── day_1.rs
│ └── day_2.rs
└── main.rs
We put the main method inside those files in the bin
folder:
// src/bin/day_1.rs
fn main() {
println!("Day 1");
}
// src/bin/day_2.rs
fn main() {
println!("Day 2");
}
Now that we have multiple bins, we must specify the one we want to run. In this case we have three options, advent_of_code
that is the root binary from src/main.rs
, day_1
and day_2
that are inside src/bin
.
$ cargo run --bin advent_of_code
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\advent_of_code.exe`
Hello, world!
$ cargo run --bin day_1
Compiling advent_of_code v0.1.0 (C:\Users\andre\projects\advent_of_code)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target\debug\day_1.exe`
Day 1
$ cargo run --bin day_2
Compiling advent_of_code v0.1.0 (C:\Users\andre\projects\advent_of_code)
Finished dev [unoptimized + debuginfo] target(s) in 0.23s
Running `target\debug\day_2.exe`
Day 2
Multiple bins within Cargo.toml
In case you don’t want to have the bin folder, there’s also the possibility to specify the binary in the Cargo.toml
file. So we have the same project with a different structure
advent_of_code
├── Cargo.lock
├── Cargo.toml
└── src
├── day_1.rs
├── day_2.rs
└── main.rs
Then in the Cargo.toml
we should specify the all the three entry points:
[package]
name = "advent_of_code"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "day_1"
path = "src/day_1.rs"
[[bin]]
name = "day_2"
path = "src/_day_2.rs"
[dependencies]
You can run the same way using the cargo run --bin bin_name
.
04 Feb 2023
Tuples
Tuples are the most basic data structure that we have in Rust. It’s based on positioning, like a fixed length array.
let first_example = get_something(String::from("First Example"));
println!("Values are: {} and {}", first_example.0, first_example.1);
// This is a way to destruct the tuple into variables for easy access
let (string_value, integer_value) = get_something(String::from("Something"));
println!("Values are: {} and {}", string_value, integer_value);
That would end printing:
Values are: First Example and 0
Values are: Something and 0
It’s also possible to add names to tuples with the struct
keyword.
struct Point(i32, i32);
fn main() {
let coordinates = Point(3, 14);
print_coordinates(&coordinates);
}
fn print_coordinates(point: &Point) {
println!("Coordinates x: {} and y: {}", point.0, point.1);
}
In case you name a tuple using struct
you won’t be able to pass arbitrary tuples or tuples with the same shape but different names.
struct Point(i32, i32);
struct LatLong(i32, i32);
fn main() {
let coordinates = Point(3, 14);
print_coordinates(&coordinates);
let lat_long = LatLong(4, 5);
print_coordinates(&lat_long);
print_coordinates((12, 33));
}
fn print_coordinates(point: &Point) {
println!("Coordinates x: {} and y: {}", point.0, point.1);
}
Throws errors
error[E0308]: mismatched types
--> src/main.rs:9:23
|
9 | print_coordinates(&lat_long);
| ----------------- ^^^^^^^^^ expected struct `Point`, found struct `LatLong`
| |
| arguments to this function are incorrect
|
= note: expected reference `&Point`
found reference `&LatLong`
note: function defined here
--> src/main.rs:14:4
|
14 | fn print_coordinates(point: &Point) {
| ^^^^^^^^^^^^^^^^^ -------------
error[E0308]: mismatched types
--> src/main.rs:11:23
|
11 | print_coordinates((12, 33));
| ----------------- ^^^^^^^^ expected `&Point`, found tuple
| |
| arguments to this function are incorrect
|
= note: expected reference `&Point`
found tuple `({integer}, {integer})`
note: function defined here
--> src/main.rs:14:4
|
14 | fn print_coordinates(point: &Point) {
| ^^^^^^^^^^^^^^^^^ -------------
For more information about this error, try `rustc --explain E0308`.
error: could not compile `structs` due to 2 previous errors
Struct
Structs are a way to group fields together into a single declaration, like an object. Structs in Rust are declared as the following:
struct User { // Name of the strcut
username: String, // Fields
email: String,
active: bool,
sign_in_count: u64, // You add the trailing comma
}
Now to instantiate a new struct you can do, no need to use new
or parenthesis:
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// To declare a mutable struct just add the `mut` keyword. The entire struct is mutable, there isn't a way to only have a single field mutable
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
To declare a mutable struct just add the mut
keyword. The entire struct is mutable, there isn’t a way to only have a single field mutable.
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
In Rust you have some syntactic sugar to instantiate structs:
- Shorthand field: If you have a variable with the name of the field, you don’t have to put the field twice (name and value), just put the variable.
let email = String::from("someone@example.com");
let username = String::from("someusername123");
let mut user1 = User {
email,
username,
active: true,
sign_in_count: 1,
};
- Spread Operator: You can use other structs to build a new struct. With the spread operator
..
.
let email = String::from("someone@example.com");
let username = String::from("someusername123");
let user1 = User {
email,
username,
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("otheruser@example.com"),
..user1
};
println!("Testing Log {}", user2.email); // Will print "Testing Log otheruser@example.com"
Is worth to remind that the spread operator in Rust is different than in Javascript. In Javascript will spread all the fields from the object into the new one. In Rust what is done is that ONLY the missing fields are copied to the new object.
And with the spread operator, the ownership of the data is passed to the new object. So you won’t have access to the field in the original object.
let email = String::from("someone@example.com");
let username = String::from("someusername123");
let user1 = User {
email,
username,
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("otheruser@example.com"),
..user1
};
println!("Testing Log {}", user2.email); // Will print "Testing Log otheruser@example.com"
println!("Testing Log {}", user1.email); // Will print "Testing Log someone@example.com"
println!("Testing Log {}", user1.username); // Will not compile
If you try to compile the example above the compiler will throw the following error:
warning: unused variable: `user2`
--> src\main.rs:18:9
|
18 | let user2 = User {
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_user2`
|
= note: `#[warn(unused_variables)]` on by default
error[E0382]: borrow of moved value: `user1.username`
--> src\main.rs:22:32
|
18 | let user2 = User {
| _________________-
19 | | email: String::from("otheruser@example.com"),
20 | | ..user1
21 | | };
| |_____- value moved here
22 | println!("Testing Log {}", user1.username);
| ^^^^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `user1.username` has type `String`, which does not implement the `Copy` trait
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
warning: `structs` (bin "structs") generated 1 warning
error: could not compile `structs` due to previous error; 1 warning emitted
A struct also can have NO fields, to be just an empty declaration. This is called a Unit Type.
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Ownership of the Data
The struct should own the all the data inside, so in a struct like:
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
When the main struct is not being used anymore all the data is freed from memory, specially the username
and email
fields that are using a String
which are variable size types that are stored in the heap.
In case we try to create a struct with references you start to bump into certain issues. So if we replace the String
with &str
:
struct User {
username: &str,
email: &str,
active: bool,
sign_in_count: u64,
}
The compiler will start throwing errors:
error[E0106]: missing lifetime specifier
--> src/main.rs:5:15
|
5 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
4 ~ struct User<'a> {
5 ~ username: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/main.rs:6:12
|
6 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
4 ~ struct User<'a> {
5 | username: &str,
6 ~ email: &'a str,
|
Right now I haven’t reached the Lifetimes part, so will stop by here.
Printing Structs
Since structs have user defined shapes, isn’t that easy to simply print a struct,
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
fn main() {
let user = User {
username: String::from("Username"),
email: String::from("email@something.com"),
active: true,
sign_in_count: 2,
};
println!("Logged User: {}", user);
}
Has the compiler throwing:
error[E0277]: `User` doesn't implement `std::fmt::Display`
--> src/main.rs:19:33
|
19 | println!("Logged User: {}", user);
| ^^^^ `User` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `User`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
So what happens if we use {:?}
or {:#?}
?
error[E0277]: `User` doesn't implement `Debug`
--> src/main.rs:19:35
|
19 | println!("Logged User: {:?}", user);
| ^^^^ `User` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `User`
= note: add `#[derive(Debug)]` to `User` or manually `impl Debug for User`
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `User` with `#[derive(Debug)]`
|
4 | #[derive(Debug)]
|
error[E0277]: `User` doesn't implement `Debug`
--> src/main.rs:20:36
|
20 | println!("Logged User: {:#?}", user);
| ^^^^ `User` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `User`
= note: add `#[derive(Debug)]` to `User` or manually `impl Debug for User`
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `User` with `#[derive(Debug)]`
|
4 | #[derive(Debug)]
|
Well, rust still can’t print. Due it’s low level nature rust don’t have things like reflection to inspect objects at runtime. So we need to add Debug symbols at the structs we want to print. After adding the #derive(Debug)
to the struct we can print:
#[derive(Debug)]
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
And here’s the cargo run
result:
Logged User: User { username: "Username", email: "email@something.com", active: true, sign_in_count: 2 }
Logged User: User {
username: "Username",
email: "email@something.com",
active: true,
sign_in_count: 2,
}
Rust also has the dbg!
macro that that prints the data with extra information. So we add that to our code:
let user = User {
username: String::from("Username"),
email: String::from("email@something.com"),
active: true,
sign_in_count: dbg!(2 + 1),
};
dbg!(&user)
And we get information about the method that was called and the value that returned. with the file and line.
[src/main.rs:16] 2 + 1 = 3
[src/main.rs:19] &user = User {
username: "Username",
email: "email@something.com",
active: true,
sign_in_count: 3,
}
Methods
It’s also possible to have methods in a struct, this way you can call struct.method()
. The syntax to declare that is:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rectangle = Rectangle {
width: 13,
height: 44,
};
println!("Rectangle area: {}", rectangle.area());
}
So the functions inside the impl
part will become methods in the Rectangle
struct. One thing that functions inside the impl
block differs from regular functions is that they always have &self
as the first parameter so we can have access to the struct fields.
&self
is the shorthand for self: &Self
which is the type of the struct that you are implementing the methods for. In this case self: &Rectangle
.
You can also write functions without the &self
as first parameter, they are called Associated Functions. For example String::from
is a associated function, and as you can see they diverge on how they are called.
Creating a associated function:
impl Rectangle {
fn square(size: u32) -> Self {
return Self {
width: size,
height: size,
};
}
}
You are also allowed to have multiple implementation blocks if you want:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.height * self.width
}
fn can_hold(&self, other: &Rectangle) -> bool {
return self.height >= other.height && self.width >= other.width;
}
}
impl Rectangle {
fn square(size: u32) -> Self {
return Self {
width: size,
height: size,
};
}
}
Having multiple impl
blocks will cause the functions to be merged into a single one.
Enums and Pattern Matching
In rust enums are declared with the following syntax
enum IpAddrKind {
V4,
V6,
}
then you can instantiate them with
let ipv4 = IpAddrKind::V4;
let ipv6 = IpAddrKind::V6;
Enums can also carry a value that you pass when you instantiate them like a struct, and different enum values can have different types of values:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let host = IpAddr::V4(String::from(127, 0, 0, 1));
let hostV6 = IpAddr::V6(String::from("::1"));
You can also use structs and other enums as the enum value. An example of enum with multiple types of values.
struct Position {
x: i32,
y: i32
}
enum Message {
Quit,
Move(Position),
Write(String),
ChangeColor(i32, i32, i32),
Image { url: String, alt_text: String },
}
The Image
has named fields like a struct would have.
Just like in structs you can implement methods to an enum with the impl
block.
impl Message {
fn call(&self) {
// Method Implementation
}
}
let message = Message::Write(String::from("Something"));
message.call();
Option enum
Instead of dealing with null values rust uses the Option enum, like the Null Object Pattern, you get a concrete implementation instead of a null reference and you can chose to do what you want when you have a null value.
This advantage of using the Option enum is that you can have the compiler to check exhaustively that you handled the null case avoid problems with it.
The Option enum comes with the standard library and it’s definition is:
enum Option<T> {
None,
Some(T),
}
You can use the None
and Some
values without having to put the Option::
let a_proper_value: Option<i32> = Some(15);
let an_empty_value: Option<i32> = None;
match - pattern matching with enums
Now is clear on how to declare enums, but they are not complete with a good way to use them. That’s where the match
keyword comes. With match
you can have an exhaustive check at compile time to be sure you took care of all the cases in your enum.
Imagine that you want to route a request to a different endpoint based on an enum value
enum Stage {
Gamma,
Production,
}
fn get_stage_url(stage: Stage) -> String {
match stage {
Stage::Gamma => String::from("https://example.org/gamma"),
Stage::Production => String::from("https://production.org"),
}
}
fn main() {
println!("Using url {}", get_stage_url(Stage::Production));
}
The code above would print:
Using url https://production.org
Now this is a very simple enum, and we saw that we can have more data than just its name. match
also allows to access the values inside the enum and you can even make more complex computations inside the match
.
fn main() {
let email = get_email_by_username(String::from("username"));
match email {
Some(email) => {
send_email_to_user(email);
println!("Email sent successfully");
}
None => println!("User don't have an email"),
}
}
fn get_email_by_username(username: String) -> Option<String> {
Some(format!("{}@example.org", username))
}
fn send_email_to_user(email: String) {
println!("Email sent to {}", email);
}
Sometimes you just need to work with a small subset of values of an enum and just ignore the rest, for that you can use the _
as a default case and {}
as a void function. Imagine if you were coding an Automated Vending Machine that only accepts 1 euro and 50 cents coins, you could implement the following code.
enum Coin {
OneEuro,
Fifty,
TwentyFive,
Ten,
Five,
Two,
}
fn main() {
let mut total: u32 = 0;
let inserted_coins = [Coin::OneEuro, Coin::Ten, Coin::Ten, Coin::Fifty];
for coin in inserted_coins {
match coin {
Coin::OneEuro => total = total + 100,
Coin::Fifty => total = total + 50,
_ => {}
}
}
println!("Total: {}", total);
}
Finally if you need just a single value of an enum then you can use the if let
construct. If you were building an RPG and you want to add a feature that when the user rolls 19 they get a a buff. We get the input wrapped in a Option<i32>
and then we have to check.
fn apply_buff() {
println!("Buff applyed!!!");
}
fn main() {
let first_roll = Some(5);
let second_roll = Some(19);
if let Some(19) = first_roll {
apply_buff();
}
if let Some(19) = second_roll {
apply_buff();
}
}
The code above would print
It is also possible to access the enum fields by replacing the hard-coded value with a variable:
03 Dec 2022
Rust has two ways to store variables during runtime.
- Stack: It’s a faster way when you know the size of the variable.
- Heap: When you don’t know the size of the memory, this is slower because it will store a pointer in the stack and the memory in the heap. It also has to look for an empty space in the memory that will fit your variable.
Rules for ownership in Rust:
- Each value has an owner
- A value can only have one owner
- When the owner goes out of scope, the value is dropped
If you want to use the value from the heap in two variables you need to clone the same.
// This is not allowed
let s1 = String::from("Something");
let s2 = s1;
// In this case only s2 is valid, s1 will be dropped
// Now to use s1 and s2
let s1 = String::from("Something");
let s2 = s1.clone(); // This will clone the information from the stack and the heap into new values
This does not apply to values that only stay on the stack so you could do without having to use copy:
let string1 = "Something";
let string2 = string1;
let integer1 = 10;
let integer2 = integer1;
Ownership and Functions
When you pass a value that is in the heap to a function, that function will be the new owner of it. So you can’t continue using after calling it.
fn main() {
let text = String.from("Something");
println_something(text); // This will give ownership of text to print_something
println!("{}", text); // This would not be valid because the value was dropped
}
fn print_something(text: String) {
println!("{}", text);
}
This does not apply to values that only stay in the stack:
fn main() {
let meaning = 42;
println_something(meaning); // This makes a copy of the value in the stack
println!("{}", meaning); // This would work normally
}
fn print_something(value: i32) {
println!("{}", value);
}
Return Values
You can overcome the limitation of not being able to use a value anymore by returning the value, this will give the ownership to the caller of the function
fn main() {
let text = String.from("Something");
let other_text = println_something(text); // This will give ownership of text to print_something
println!("{}", other_text); // This would not be valid because the value was dropped
}
fn print_something(text: String) -> String {
println!("{}", text);
text // without semicolon to return the value
}
References & Borrowing
Sometimes you want to use some value from the heap in a function but you don’t want to take ownership of it. Example:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
For calculate_length
you don’t want to take ownership of the string
. In this case you just want to read the value and return the length. To solve this problem you can use a reference.
& is the symbol used for passing references
So after using a reference our function would be:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
In Rust passing a reference is called Borrowing
and it allows you to keep the ownership because when you passing a reference, you don’t transfer the ownership to the function. With Borrowing you can’t mutate values. So if you write
fn main() {
let mut to_be_mutated = String::from("Hello");
append_dot(&to_be_mutated);
println!("{}", to_be_mutated);
}
fn append_dot(text: &String) {
text.push_str(".");
}
The compiler will throw the following exception:
warning: variable does not need to be mutable
--> src/main.rs:10:9
|
10 | let mut to_be_mutated = String::from("Hello");
| ----^^^^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
error[E0596]: cannot borrow `*text` as mutable, as it is behind a `&` reference
--> src/main.rs:28:5
|
27 | fn append_dot(text: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
28 | text.push_str(".");
| ^^^^^^^^^^^^^^^^^^ `text` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`.
You can’t mutate what you don’t own.
Mutable References
Rust does allow to have mutable references but it has a specific syntax for it.
&mut
is used when you need a mutable reference
So we fix the previous example to allow mutable references.
fn main() {
let mut to_be_mutated = String::from("Hello");
append_dot(&mut to_be_mutated);
println!("{}", to_be_mutated);
}
fn append_dot(text: &mut String) {
text.push_str(".");
}
Now the code above would be working.
Mutable References has some limitations, you can’t borrow a mutable reference twice at the same time. So if you try the following piece of code:
fn main {
append_text(&mut to_be_mutated, &mut to_be_mutated);
}
fn append_text(original: &mut String, appended: &mut String) {
original.push_str(appended);
}
This piece of code will give the error when ran:
error[E0499]: cannot borrow `to_be_mutated` as mutable more than once at a time
--> src/main.rs:14:37
|
14 | append_text(&mut to_be_mutated, &mut to_be_mutated);
| ----------- ------------------ ^^^^^^^^^^^^^^^^^^ second mutable borrow occurs here
| | |
| | first mutable borrow occurs here
| first borrow later used by call
For more information about this error, try `rustc --explain E0499`.
This limitation is how rust keep the language safe. This is to avoid data races at compile, since only one mutable reference can exist at any time. Rust also has check when you are using mutable and immutable references together. If you try to use them together the compiler will throw an error.
fn main {
let mut mixed_type_values = String::from("Mixed Type Values");
let s1 = &mixed_type_values;
let s2 = &mixed_type_values;
let s3 = &mut mixed_type_values;
println!("Print vals: {}, {} and {}", s1, s2, s3);
}
error[E0502]: cannot borrow `mixed_type_values` as mutable because it is also borrowed as immutable
--> src/main.rs:17:14
|
15 | let s1 = &mixed_type_values;
| ------------------ immutable borrow occurs here
16 | let s2 = &mixed_type_values;
17 | let s3 = &mut mixed_type_values;
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
18 | println!("Print vals: {}, {} and {}", s1, s2, s3);
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
Now if you use the immutable values before, it will work, because those references don’t have any risk of being mutated before the usage.
fn main() {
let mut mixed_type_values = String::from("Mixed Type Values");
let s1 = &mixed_type_values;
let s2 = &mixed_type_values;
println!("Print vals: {} and {}", s1, s2);
let s3 = &mut mixed_type_values;
println!("Print val: {}", s3);
}
Check Non Lexical Lifetimes
Dangling References
A Dangling Reference is when you have a pointer referencing a memory address that was freed from memory. So, that pointer points to nothing. The following code creates a dangling reference:
fn main() {
let reference_to_nothing = dangle(); // 3. Now we have a dangling reference
}
fn dangle() -> &String {
let s = String::from("hello"); // 1. Create the value in memory
&s // 2. returns the reference and free the value of s
}
The compiler will throw:
error[E0106]: missing lifetime specifier
--> src/main.rs:45:16
|
45 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
45 | fn dangle() -> &'static String {
| +++++++
For more information about this error, try `rustc --explain E0106`.
The solution is quite easy, you just return the real value.
fn main() {
let reference_to_s = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
s
}
In this case the ownership of s
will be given to reference_to_s
.
Rust will only allow one single mutable reference per time or multiple immutable references. This is how race conditions are avoided at compile time. The compiler also will not allow you to have invalid references.
Slice
Slices are a way to pass a reference of a subset of a collection. This new slice will behave like a new collection having the .len()
and it’s 0
and last indexes to be in the beginning of the slice.
The syntax is quite simple:
let items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let slice = &items[0..3]; // This will be [0, 1, 2] the end index is exclusive
And you can do the same with strings:
fn main() {
let items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let first_half = &items[0..5];
let second_half = &items[6..10];
println!(
"First Half - First Item: {}, Length: {}",
first_half[0],
first_half.len()
);
println!(
"Second Half - First Item: {}, Length: {}",
second_half[0],
second_half.len()
);
let hello_world = "Hello, World!";
let hello = &hello_world[0..5];
let world = &hello_world[7..12];
println!("This is {}, and this is {}", hello, world);
}
The code above would print:
First Half - First Item: 0, Length: 5
Second Half - First Item: 6, Length: 4
This is Hello, and this is World
Now imagine that you have to check if an element exists in a sorted array, you can use Binary Search for that. You start with an array, then you call the same method recursively on the left and right side. Usually you would have to pass the start and end indexes to it.
The implementation would end like this:
fn main() {
let items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let found = binary_search(&items, 3, 0, 11);
println!("Is item 3 in the array? {}", found);
}
fn binary_search(items: &[i32], target: i32, start_index: usize, end_index: usize) -> bool {
// Implementation hidden for obvious reasons
}
Now if we start using slices we can change the implementation to be
fn main() {
let items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let found = binary_search_with_slices(&items, 3);
println!("Is item 3 in the array? {}", found);
}
fn binary_search_with_slices(items: &[i32], target: i32) -> bool {
// Implementation hidden for obvious reasons
}