This project will cover a bit on errors, as well as learning about some systems in Rust.
We will be simulating a bank with many accounts.
Each account will have a balance, account_number (or id) and holder property.
Start with defining Account
and Bank
.
#[derive(Debug)] struct Account { balance: i32, id: i32, holder: String, } #[derive(Debug)] struct Bank { accounts: Vec<Account>, }
Bank:
Description | Method/associated func? | Name | Args | Returns |
---|---|---|---|---|
Creates Bank instance | AF | new() | - | Bank |
Account:
Description | Method/associated func? | Name | Args | Returns |
---|---|---|---|---|
Creates Account instance | AF | new() | id: u32, holder: string | Account |
At this point, we update our main
function:
fn main() { let bank = Bank::new(); println!("{:#?}", bank); let account = Account::new(1, String::from("Me")); println!("{:#?}", account); }
For now, we're going to refactor our the println!
call as a helper function.
fn print_account(account: Account) { println!("{:#?}", account); }
If we have the following code:
fn main() { let bank = Bank::new(); println!("{:#?}", bank); let account = Account::new(1, String::from("Me")); print_account(account); print_account(account); // USE AFTER MOVE ERROR }
We'll get a use after move error. Why is that? The next section covers it.
To understand this, we need to understand three Rust concepts:
They are three connected systems which can be tough to understand, but represent 90% of the difficulty of Rust.
They will dramatically change the way you will design and write code (compared to other languages).
The rules:
These rules dramatically change how you write code. When it doubt, remember that Rust wants to minimize unexpected updates to data.
The goal of ownership is to limit the ways you can reference and change data.
This limitation will reduce the number of bugs + make your code easier to understand.
There is an example of a tiny program in JavaScript vs Rust where it's impossible to happen in JavaScript.
const engine = { working: true } const mustang = { name: "Mustang", engine: engine } const camero = { name: "Camero", engine: engine } function checkCar(car) { if (car.name === "Mustang") { car.engine.working = false } } checkCar(mustang) // As expected, mustang.engine.working === false // !!! BUT OH NO camero.engine.working === false
The above code demonstrates how we've easily mutated the state of the Camero engine without any resistance from JavaScript. This happens because of the reference that the mustang
and camaro
are referencing.
Let's see this code, but this time in Rust.
The fix this bug, we have two options:
engine
becomes read-only, but we can't modify it.engine1
and engine2
.The lessons we can get from this:
Those two lessons for the basis of the ownership + borrowing system. These rules are implemented in Rust with the goal of reducing bugs like we have seen with the car example.
Lesson (1) is connected to our list of rules for (3), (5) and (6).
Lesson (2) is connected to (1), (5) and (6).
This will work through rule list (1) and (2).
Rust wants to stop you from unexpected updates.
The following code violates the rules of (1) and (2):
fn main() { let bank = Bank::new(); let other_bank = bank; println!("{:#?}", bank); }
This is because of our two bindings. Effectively, other_bank
is saying "take what's in the bank binding and move it to the other_bank binding".
When the value is moved, then bank
has no binding at all. So when we run println!
, we're trying to print a value that's already been moved.
Let's expand on rule (1) and (2) with their full versions.
An owner can be owned by a single variable, argument, struct, vector etc.
Reassigning a value to variable, passing it to a function, putting it into a vector etc moves the value. The old owner can't be used to access the value anymore!
Given those rules, we know the following won't work:
fn print_account(account: Account) { println!("{:#?}", account); } fn main() { let account = Account::new(1, String::from("Me")); print_account(account); print_account(account); }
Another example of things not working due to move to a vector:
fn main() { let account = Account::new(1, String::from("Me")); let list_of_accounts = vec![account]; println!("Here's your account: {:#?}", account); }
Another example due to a reassignment:
let bank = Bank::new(); let accounts = bank.accounts; println!("{:#?}", bank.accounts);
Another because of the movement of the account reference to being owned by the print_account
function:
fn main() { let account = Account::new(1, String::from("Me")); print_account(account); println!("{}", account.holder); }
Finally, let's look at an example where a property of a struct is moved which causes an error later:
fn print_account(account: Account) { println!("{:#?}", account); } fn print_holder(holder: String) { println!("{}", holder); } fn main() { let account = Account::new(1, String::from("Me")); print_holder(account.holder); print_account(account); }
Rust doesn't even allow you to use values that have been partially moved!
Given the ownership system, we have two options:
For (1):
fn print_account(account: Account) -> Account { println!("{:#?}", account); account } fn main() { let mut account = Account::new(1, String::from("Me")); account = print_account(account); account = print_account(account); println!("{:#?}", account); }
But it's not super useful, so let's explore borrowing.
Start with a code snippet:
fn print_account(account: &Account) { println!("{:#?}", account); } fn main() { let account = Account::new(1, String::from("Me")); let account_ref = &account; print_account(account_ref); // equivalent to just put in &account here println!("{:#?}", account); }
We use the &
to get a reference to the value. We then use that reference to print into the account.
THe &
has different uses depending on where you put it.
&
being used on a type = This argument needs to be a reference to a value of this type&
being used on a value = I want to create a reference to this valueThere are two kinds of references:
The example we had before was a read-only reference. We couldn't use that reference to change the value.
Another important rule we mentioned is that you can't move a value that has a ref.
fn main() { let account = Account::new(1, String::from("Me")); let account_ref = &account; let out_account = account; print_account(account_ref); println!("{:#?}", account); }
In this case, "cannot move out of account
because it is borrowed
move out of account
occurs here" happens when we try reassigning account
to out_account
while account_ref
exists.
Moving ownership to update something can be really tedious.
That the following code that rebinds account:
fn change_account(account: Account) -> Account { account.balance = 10; account } fn main() { let mut account = Account::new( 1, String::from("me") ); account = change_account(account); println(":#?}", account); }
We can use mutable references to help resolve this:
fn change_account(account: &mut Account) { account.balance = 10; } fn main() { let mut account = Account::new(1, String::from("Me")); change_account(&mut account); println!("{:#?}", account); }
There are some important rules about this you need to understand:
Any example of the ref issue:
// Will not compile fn main() { let mut account = Account::new(1, String::from("Me")); let account_ref = &account; change_account(&mut account); println!("{:#?}", account_ref.holder); }
As for the second problem, we can demonstrate it this way:
// This won't compile fn main() { let mut account = Account::new(1, String::from("Me")); let account_ref = &mut account; account.balance = 10; // WON'T WORK WITH THE REF EXISTING AND BEING MUTATED change_account(account_ref); println!("{:#?}", account); }
From rule (7) placed at the start: Some types are copied instead of moved (numbers, bools, chars, arrays/tuples and copyable elements).
Another way to think of this is that some values will break the rules of ownership.
Take the following code:
// THIS DOES NOT COMPILE BECAUSE OF OWNERSHIP ISSUES fn main() { let account = Account::new(1, String::from("Me")); other_account = account; println!("{:#?}", account); }
But what happens if we have this?
// This works fine! fn main() { let num = 5; let other_num = num; println!("{} {}", num, other_num); }
The types that are copied instead of moved:
For example with arrays:
fn main() { let num_arr = [1, 2, 3]; let other_num_arr = num_arr; println!("{:#?} {:#?}", other_num_arr, num_arr); }