DEV Community

Cover image for Learning Rust πŸ¦€: 07 - Ownership: Memory Dynamics
Fady GA 😎
Fady GA 😎

Posted on • Updated on

Learning Rust πŸ¦€: 07 - Ownership: Memory Dynamics

In the last article, I said that I'd split the Rust Ownership topic over 2 articles. But this will be a bit hard in order for the articles to be short and concise. So, I'll split the Ownership topic over 3 or maybe 4 articles.

⚠️ Remember!

You can find all the code snippets for this series in its accompanying repo

If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page. But for this one, you need a cargo installation on your machine (or a VM).

⚠️⚠️ The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.

⭐ I try to publish a new article every week (maybe more if the Rust gods πŸ™Œ are generous 😁) so stay tuned πŸ˜‰. I'll be posting "new articles updates" on my LinkedIn and Twitter.

Table of Contents:

The String type:

So far, we have been using types with known sizes during compile time and those get stored in the Stack. In order to demonstrate further Rust's Ownership, we have to use a type that gets stored in the Heap. The String type is one of those types. In general, a pointer to the Heap memory block that is "allocated" to a String variable and its length are stored in the Stack and the actual data is stored in the Heap. A String variable can be of unknown size at compile time and it can be mutable because - as pointed out - that it reserves a place in the memory that can grow and shrink unlike the string literals which are hardcoded in the final executable and therefore with known size hence they are stored in the Stack.

let mut hello = String::from("Hello"); // A new String variable using the "from" method from the String namespace.
hello.push_str(", World!"); // Appends a string to the same String variable.
println!("{hello}");
Enter fullscreen mode Exit fullscreen mode

Memory and allocation:

For type like the String which is of unknown size at compile time, we have to do two things:

  1. Request the needed memory at runtime.
  2. Release this memory when we are done using the String.

The Request part is done by us. We did that when we called the "String::from" method in the previous example. The second part is where Rust's Ownership comes to play. For languages that use "garbage collection" mechanisms, the GC (Garbage Collector) takes care of this. For other languages (other than Rust), the developer has to do both the request and release parts with the latter proving to be challenging. As forgetting about releasing the memory will waste it. And doing it too soon will result in invalid variables. Also doing it twice is a bug. So in short,

We need to pair exactly one allocate with one release.

Rust, however, releases the requested memory on your behalf. It does so whenever a variable goes out of scope.

The Move and the Copy:

The memory release rule seems simple enough but imagine this scenario:

let s1 = String::from("String Value");
let s2 = s1;
println!("S2: {s2}");
println!("S1: {s1}");
Enter fullscreen mode Exit fullscreen mode

For heap stored types (if we can call them that), a statement like let s2 = s1; doesn't duplicate the data of s1 in the heap and associate the new copy with s2 as this would be expensive at runtime (imagine a very very large String that consumes a large portion of the memory get duplicated!). Instead, Rust keeps the Heap data intact and just creates another pointer to it in s2. This presents a problem, Rust releases the allocated memory once the variable goes out of scope. So, suppose that s1 goes out of scope first, then Rust releases the heap data. Soon after, s2 goes out of scope, then Rust supposedly tries to release the heap data which was already released when s1 went out of scope. This is the "double release" problem mentioned in the previous section.

What Rust does to enforce memory safety for situations like these, it invalidates s1 in the previous example after let s2 = s1. So println!("S1: {s1}") will cause a compile error.

It is said that s1 has been MOVED to s2.

But this isn't how Rust behaves with simple types like integers! For type with known size at compile time, Rust doesn't move the variable in case of an assignment statement like the previous one. It COPIES the variable as it is already stored in the Stack which is easier to copy. So, the following example is valid:

let n1 = 10;
let n2 = n1;
println!("n1: {n1}"); // is valid.
println!("n2: {n2}");
Enter fullscreen mode Exit fullscreen mode

It is said that n1 has been COPIED into n2.

The Clone:

But what if duplicating Heap data is just what we want and we don't want to move variables? Rust has the "clone" method for that:

let s3 = String::from("Clone");
let s4 = s3.clone();
println!("s3: {s3}"); // is valid.
println!("s4: {s4}");
Enter fullscreen mode Exit fullscreen mode

This method clones the Heap data which is - as mentioned earlier - can be very expensive at runtime so use it carefully.

I'll have to stop here to keep it short. In the next article, we will discuss Ownership and Functions! See you then πŸ‘‹.

Top comments (0)