The only (beginner) Rust tutorial you will ever need.

Ryan L. Kopf
4 min readFeb 2, 2024

--

Often people try to teach you Rust by asking you to read the Rust book, or they dive into complicated technical tutorials. If you’re an experienced programmer, these things can be frustrating, so you just jump into using a complex system like Bevy to try to make a game or Actix to try to make a web server.

And then you encounter all kinds of issues. “Can’t use ‘health_bar’ because it is already used on line 26, 24”. “Can’t borrow variable as mutable and immutable at the same time.” Etc.

And then people tell you to read the book on borrow checking. But books are boring. I don’t need to read how “This guide is two of three presenting Rust’s ownership system. This is one of Rust’s most unique and compelling features, with which Rust developers should become…” blah blah blah.

Honestly, the book is well written and concise — particularly compared to other languages. But still, a “book” is daunting. You don’t need that. You just need to be able to answer one question. This question you will ask and answer over and over and over when writing Rust. Get good at this question, and you solve ALL your Rust problems.

The question to ask when you write a function:

Do I need this ‘variable’?

Not just, “Do I need to use this variable”, but more of a “Do I need to keep and own this object.”

Picture this example:

You’re building a function that takes a Player struct as a parameter. This Player has a health_bar attribute. If your function only needs to read the health_bar value, you do not “need need” the variable. You only need the “reference” to the variable. You need to know that the variable exists, and what the values are inside it, but you don’t need to package it up and keep it forever inside your function.

fn display_health(player: Player) {
// Bad! This "takes" the Player
println!("Health: {}", player.health_bar);
}

And instead:

fn display_health(player: &Player) {
// Good! You don't need to "take" the Player
println!("Health: {}", player.health_bar);
}

If don’t need ownership, you “borrow” the item by “reference”. Use &Player to borrow the Player struct.

// BAD!
struct Player {
health_bar: u32,
}

fn display_health(player: Player) {
println!("Health: {}", player.health_bar);
}

// Usage
let player = Player { health_bar: 100 };
display_health(player);
// 'player' can no longer be used here as its ownership has been transferred to 'consume_player'
display_health(player); // <!-- This will error, because you "took" Player away

Instead, just take a reference:

// GOOD!
struct Player {
health_bar: u32,
}

fn display_health(player: &Player) {
println!("Health: {}", player.health_bar);
}

// Usage
let player = Player { health_bar: 100 };
display_health(&player);
display_health(&player); // <!-- This works fine

But what if your function needs to update the health_bar? In that case, you need mutable access. In order to mutably modify something, you need to make sure something else is neither trying to “borrow” nor “take” the same thing at the same time.

struct Game {
pub monsters: Vec<Entity>,
pub players: Vec<Entity>
}
// This errors!
impl Game {
pub fn damage_players(&mut self) {
for player in &mut self.players { // <!-- Note
for monster in &mut self.monsters {
if monster.at_player(&player) {
player.hp -= monster.strength;
monster.xp += 1;
if self.player.hp < 0 {
self.players.retain{|p|p.id != player.id}
// Big 'ol error here. Why? Because you are
// going through the 'players' 1 by 1, but
// then you try to remove 1 at the same time!
// IE: Two mutable references, can't do it!
}
}
}
}
}
}

The above shows you how you can’t have two mutable references at once. Here’s one way to solve this:

struct Game {
pub monsters: Vec<Entity>,
pub players: Vec<Entity>,
}

impl Game {
pub fn damage_players(&mut self) {
let mut to_remove = Vec::new();
for player in &mut self.players {
for monster in &mut self.monsters {
if monster.at_player(player) {
player.hp -= monster.strength;
monster.xp += 1;
if player.hp < 0 {
to_remove.push(player.id);
}
}
}
}
// Remove players after iterating
self.players.retain(|p| !to_remove.contains(&p.id));
}
}

Another way to do it:

impl Game {
pub fn damage_players(&mut self) {
// Clone the players for iteration
let players_snapshot = self.players.clone();
for player in players_snapshot.iter() {
for monster in &mut self.monsters {
if monster.at_player(player) {
self.players.retain(|p| p.id == player.id && p.hp > monster.strength)
if let Some(original_player) = self.players.iter_mut().find(|p| p.id == player.id) {
original_player.hp -= monster.strength;
monster.xp += 1;
}
}
}
}
}
}

Although the above is not the most idiomatic.

Understanding when to borrow, when to borrow mutably, and when to take ownership is key. This decision-making process underpins Rust’s memory safety guarantees. It eliminates common bugs found in other languages related to concurrent data access and memory management.

In summary, always start with the question: “Do I need ownership of this variable or can I just borrow it?” Your answer determines how you interact with the data, adhering to Rust’s strict, but beneficial, ownership and borrowing rules. This mindset shift is what will elevate your Rust programming from frustrating to fluent.

--

--

Ryan L. Kopf
Ryan L. Kopf

Written by Ryan L. Kopf

Serial C.E.O. and Entrepreneur. Great at technology, innovation, and entertainment arts.

No responses yet