DEV Community

Cover image for Rustlings IV (Ownership)
Blitty
Blitty

Posted on • Edited on

Rustlings IV (Ownership)

This is the most difficult part in my opinion. Because is totaly different from other languages like C. And is kind of interesting too.

So...
lets do it

First of all, everything I write here can be found in the docs

OwnerShip ๐Ÿ‘€

What is ownership?

Ownership is a set of rules that governs how a Rust program manages memory.

I think the definition is easy, also as you will see, this "rules" prevents us from using the debugger too much which is cool because u've controlled "stupid" things before running!!!

Docs talks about Heap and Stack which are important!! I am going to talk a little about them.
As u should know both, the stack and the heap are data structures.

First the stack. What is it?

Well the name says everything, a stack is a data structure which can either pop or push. When u push a new value, it is stacked at the top and when u pop a value, it removes the top value. You can only access the top value, so to get the first value you need to pop everything that is over it.

Something like this:
stack like

Why do we need to know about it??
Well... because things like, when you call a function, its parameters and local variables are pushed to the stack. When finished they are poped.

Maybe you don't see why you need this, but is not "you need or not". It is done for simplicity.

If you have had program in assembler, you can understand why we need it. You would have used it to push a value you will need later, but we don't do this in rust.

Also each process has its own stack and heap at run time.

Okey, hope everything is:
okey

Now... what is the heap?

As we said earlier, it is a data structure, so we can use it to store "values". It is less efficient than stack for some things, because when you create a new varible, you need to search for a space that is not being used to store that value.

So like docs says "is a little messy", and when you need to get a value, you have to follow the pointer. It is unefficient but, another point of view, it is good because is much more flexible than the stack.

Also the heap is used for dynamic allocating.

More info about Heap and Stack

Continue
(I don't really drink coffe...)

This are the owner ship rules:

  • Each value in Rust has a variable thatโ€™s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Remember them because you will need it uwu

When we are doing -->

let s1 = String::from("hello");
Enter fullscreen mode Exit fullscreen mode

we are creating something like this -->

name value
ptr ->
len 5
capacity 5
index value
-> 0 h
1 e
2 l
3 l
4 o

(ptr points to index 0)

If you come from C, you know that you are able to allocate dynamic memory with malloc, calloc, etc but you are responsible for "freeing" that memory, and you use free. Here comes one thing, if you allocate memory and also make a reference to it (a pointer), when you free that pointer, you are "freeing" that memory which is pointing. You have to be careful with this because if you free the variable (not the pointer), you will be "freeing" something that is already free, so you will collapse the program.

So it is a bit difficult because you are, maybe wasting a lot of memory or you are just de-allocating memory everywhere, so it's kind of difficult and it takes experience. But once you get it, everything is okey.... I think....

// test.c
#include <stdio.h>
#include <stdlib.h>

int main ()
{
  int *o = (int *) malloc(sizeof(int));
  *o = 5;

  int *e = o;

  printf("%d - %d\n", *o, *e);
  free(e);
  printf("%d - %d\n", *o, *e);
  free(o); // error

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

This is the output:
test.c solve

Also you can try this code on Repl.it

I think this is clear enough.
Now lets talk about languages like Java that has a Garbage Collector (GC) which free the memory when that part of memory is not used. Of course having a GC is okey (?) if you don't want to play with memory and make your prorgam explode. But... meh.

What Rust does is simply de-allocate that memory that has been used when the scope has end. A scope is, for example, a function, or the main, as well coding blocks.

If you don't know what a block is, here is an example:

fn main ()
{
  { // start of block and a new scope
    let x = 9;
  } // end of the scope, so x is being "free" (de-allocated)
}
Enter fullscreen mode Exit fullscreen mode

You need to take that in consideration that. In depth, what Rust does is call a function drop which basically de-allocates memory.

In my opinion, this approach is much better because you just have to control when you need that value from the scope.

Remember the c code whe saw later? Well, now you will see what Rust do if you make something similar, like:

fn main ()
{
  let x = String::from("Hello");
  let y = x;
}
Enter fullscreen mode Exit fullscreen mode

Rust compiler will tell you:
Rust says

Because in Rust you cannot have two variables which points to the same "thing" unless they are integers, booleans, etc, primitives.

This is better explained in the book:

If youโ€™ve heard the terms shallow copy and deep copy while working with other languages, the concept of copying the pointer, length, and capacity without copying the data probably sounds like making a shallow copy. But because Rust also invalidates the first variable, instead of calling it a shallow copy, itโ€™s known as a move.

This means that our x will be "disabled". And I think this is cool because it prevents us to make junk code xd.

But if you really need a copy of it, you can use clone. I think the docs are okey, so not gonna explain anything.

OOOOOHHHH ALSO you can "copy" tuples whiout the "clone" method, when it is formed of primitives, like we said earlier.

(i32, bool) // implicit Copy
(u32, String) // no implicit Copy because of String
Enter fullscreen mode Exit fullscreen mode

Ownership in functions should be easy to understand with what I have explained, have a read

Borrowing ๐Ÿฅท

Borrowing is the action of creating a Reference. Becuase once you get that reference, you return it.

When you "borrow" a variable, you cannot change its value. Unless is a "mutable borrow".

According to the book

Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type.

// example from book
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 is a reference
  s.len()
}
Enter fullscreen mode Exit fullscreen mode

Look at the image

By using references, you don't get the ownership of that variable, so it is cool. Less mess.

You can have mutable references, but you have to be careful, because you can only have one.

// creating a mutable reference
let r1 = &mut s;
Enter fullscreen mode Exit fullscreen mode

Now we said that you cannot have 2 mutable references, that is not quite true. You can have more than one mutable reference, but they have to be either:

  • Different scopes
  • The mutable reference is not used after the next mutable reference is created
// this is valid
fn main ()
{
  let mut s = String::from("hello");

  let r1 = &mut s;
  r1.push_str(", world");
  println!("{}", r1);

  let r2 = &mut s;
  r2.push_str("!!!");
  println!("{}", r2);
}
Enter fullscreen mode Exit fullscreen mode
// this is invalid
fn main ()
{
  let mut s = String::from("hello");

  let r1 = &mut s;
  let r2 = &mut s;
  r1.push_str(", world");
  r2.push_str(", world");

  println!("{}, {}", r1, r2);
}
Enter fullscreen mode Exit fullscreen mode

Maybe this is weird and you don't get it. Rust says:

The benefit of having this restriction is that Rust can prevent data races at compile time.

And a data race is like race conditions when using paralelism. A data race can appear:

  • Two or more pointers access the same data at the same time.
  • At least one of the pointers is being used to write to the data.
  • Thereโ€™s no mechanism being used to synchronize access to the data.

You can have a lot of inmutable references. BUTTTT be careful, when using references, if you have inmutable, you cannot have mutable ones on the same scope. The answer to what you are thinking is that you are not able to change the data while someone is reading it.

It is like when using databases. There is always (if it is not well done) a way of having an error value. Imagine that Paula wants to read, and indeed is reading but then suddenly at the same time Jose is writting, the value has change, so Paula has an obsolete value, which is not cool.

(Is it clear? I wish it is)

The least: Dangling References. A Dangling reference, is something like a value being lost when it shouldn't because you want that value. This happens when you want to return a value from a scope, but not actually returning the value, instead you are returning the reference. As we said earlier, when a scope ends, the variables inside it are "dropped" (removed), so you cannot return a reference to something that is going to be dropped.

Example:

fn main() {
  let reference_to_nothing = dangle();
}

// F
fn dangle() -> &String {
  let s = String::from("hello");

  &s
}

// this is better :D
// you are retturning the value not the reference, so nice.
fn no_dangle() -> String {
  let s = String::from("hello");

  s
}
Enter fullscreen mode Exit fullscreen mode

Slices ๐Ÿ•

If you have code in python, this is going to be familiar. Types of slices:

  • String literals
  • Arrays

First of all, we need to clarify that slices does NOT have Ownership, so you can do almost whatever. If you remember, we couldn't have more than 1 reference to something. But with slices, we can have lots of references!!

fn main()
{
  let my_string = String::from("hello world");
  let a = [1, 2, 3, 4, 5];

  let word0 = &my_string[0..6]; // from index 0 to index 6
  let word1 = &my_string[..]; // from 0 to the length
  let word1 = &my_string[..3]; // from index 0 to index 3

  // this is bad
  // .clear uses a mutable reference, and we have inmutable references
  s.clear(); // error!

  println!("the first word is: {}", word0);
}
Enter fullscreen mode Exit fullscreen mode

The concepts of ownership, borrowing, and slices ensure memory safety in Rust programs at compile time.

Done
Whish you understood everything!! Because is one of the most fundamental parts in my opinion and I am still learning it uwu


Follow me!

๐Ÿฆ Twitter
๐Ÿ™ GitHub
๐Ÿ‘ฅ LinkdIn


Top comments (0)