Learning Rust #6: Understanding ownership in Rust
Last December I finally started learning Rust and in January I built and published my first app with Rust: 235. Learning Rust is my monthly blog series that is defnitely not a tutorial but rather a place for me to keep track of my learning and write about things I've learned along the way.
Learning Rust series
- Learning Rust #1: Pattern Matching
- Learning Rust #2: Option & Result
- Learning Rust #3: crates.io & publishing your package
- Learning Rust #4: Parsing JSON with strong types
- Learning Rust #5: Rustlings
- Learning Rust #6: Understanding ownership in Rust (you are here)
- Learning Rust #7: Learn from the community
- Learning Rust #8: What's next?
- Learning Rust #9: A talk about rustlings
- Learning Rust #10: Added new feature with a HashMap
Learning Rust talk in Rust Denver meetup
Before we jump into this month's topic, I wanted to let you know that I gave a talk last month in Rust Denver meetup. The talk was about basically the same things as this blog series: what I've learned in my ~7 months of Rust and especially how those things reflect against my background as a mainly Python and Javascript developer.
Go watch the talk in my Youtube channel where I also host codebase livestreams once a month and then come back to finish reading this blog post!
Garbage collection – and the lack of it
I've been programming mostly in languages like Python and Javascript, both which have automated garbage collection. What it means is that as a developer, I don't have to worry about whether a variable should stay in memory or not, the compiler or interpreter takes care of that.
And in very practical terms, what it means is that with Python or Javascript,
I don't have to think about how to refer to variables. It's always
just name
. That is not the case with Rust.
My first touch to languages where I had to think about these things was a
Systems Programming course that I took a decade ago in the university. That
course focused on C and C++. Back then, I really struggled with gaining
routine on when to use the variable as-is, like items
and when to
use a reference, like &items
. Somehow, I ended up passing
that course but also didn't do anything more with C and C++ since.
I feel like I understand the theory and when I read about the topic, I keep nodding. Sure, this seems simple, of course it's like that. And then I get back to code and I get told by the compiler that I did not, in fact, understand it.
Stack and Heap
To understand how memory management works in Rust, we need to take a look at two concepts: the stack and the heap. I won't go into it in-depth here but if you want to learn more, there's a good explanation in the Rust book and visualization in Technorage's blog.
In a nutshell, Rust stores memory into its stack and its heap. Stack is very fast to allocate to and access from and it's the default place where Rust stores your application's memory. Rust uses stack to store local variables with limited scope.
Heap is used to store values that are passed between functions or have longer lifetime than just the local scope. It's slower to access though which is why Rust uses stack by default.
Ownership in Rust
Ownership is Rust’s most unique feature, and it enables Rust to make memory safety guarantees without needing a garbage collector. - Rust Book
Rust is often praised for how it manages its memory through a system ownership and how its borrow checker helps the developers to not make mistakes where you'd be trying to access something that doesn't exist anymore.
This first example comes from the Rust Book:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
}
If you try to run this code, it results in an error. That's because a move occured and on line 5, we're trying to access a value that has been moved on line 3.
When you do let s2 = s1
, it does not copy the value nor the
reference of s1
. Instead, it moves the reference and causes
s1
no longer to point to anything so we cannot access it anymore
inside the println
macro.
A move happens in assignment, passing into or returning from functions, when assigned to a member in struct or when explicitly called with functions that move ownership.
However, that's not always the case. In our above example, we used
String
which gets moved on assignment but if a type implements
Copy
trait, the value is copied, like with type u32
:
fn main() {
let num: u32 = 150;
let num_copy = num;
println!("{:?} == {:?}", num, num_copy);
}
HashRust has a nice blog post on the topic and goes a bit deeper than I did here.
Understanding this has been the first step for me. For someone coming from languages where this doesn't happen, it's been quite a challenge to internalize.
Borrow Checker
Rust comes with a borrow checker tool that helps you avoid these mistakes. It ensures that every access to memory is valid.
Let's take a look at a bit more involved example (this one will result in compiler error):
fn reverse(word: String) -> String {
word.chars().rev().collect::<String>()
}
fn main() {
let word = String::from("example");
let reversed = reverse(word);
println!("{:?} is reversed to {:?}", word, reversed);
}
Here we have a function reverse
that accepts a
String
and returns another String
.
Inside our main
function, we pass word
into
reverse
function which causes it to be moved and no longer
accessible after line 7. We have multiple ways to fix this.
First one is to accept a reference (&String
) and pass such
reference (&word
) into the function:
fn reverse(word: &String) -> String {
word.chars().rev().collect::<String>()
}
fn main() {
let word = String::from("example");
let reversed = reverse(&word);
println!("{:?} is reversed to {:?}", word, reversed);
}
Once we run this, we'll get output
"example" is reversed to "elpmaxe"
. Sure, in Rust you could
simplify this even further by using &str
but since that's
kinda a special case with strings, I wanted to keep this example more
generically applicable. If you wanna learn more about those, I recommend
checking out
Tomas Buß's blog post Rust Slice Types (and Strings), explained.
Another way to fix the issue is to create a clone of the original variable.
Cloning is available for all types that implement
Clone
trait. When cloning a variable, Rust creates a deep copy* so each of the variables
point to a different point in memory.
* Kinda, at least. Developers are welcome to implement clone as they wish but deep copy is often the default behaviour implemented.
fn reverse(word: String) -> String {
word.chars().rev().collect::<String>()
}
fn main() {
let word = String::from("example");
let reversed = reverse(word.clone());
println!("{:?} is reversed to {:?}", word, reversed);
}
If you want to deep a bit deeper into understanding Borrow Checker system, I recommend watching Nell Shamrell-Harrington's talk The Rust Borrow Checker: a Deep Dive from RustLab. Tim McNamara also did a nice video on the topic last week. And finally, LogRocket has a blog post written by Thomas Heartman called Understanding the Rust borrow checker that has been very helpful for me.
What's so hard about this then?
As I was reading more on the topic and writing this blog post, I once again felt like I understand these concepts but when I actually write code, I still struggle quite a bit with it.
I believe one key reason for that is that I don't yet have an internalized mental model of thinking about these things since I've coded for a better part of a decade without ever thinking about them in other languages. So when for example I try to map over a vector of items, do operations on them and return something, things start to feel very complicated. Things I'm very comfortable doing in Python or Javascript suddenly become complicated in Rust.
I hope it's something that I'll get better at just by writing more Rust. As I'm fixing the final bugs and issues in 235, I'm currently planning on what project to pick up next to continue learning the language. I have a lot of interest for static site generators and building one in Rust is something that interests me for various reasons but I'm not sure if it forces me to use more complex features of the language enough to really push my learning to the next level.