Rustlings IV (Ownership)
Blitty
Posted on January 31, 2022
This is the most difficult part in my opinion. Because is totaly different from other languages like C. And is kind of interesting too.
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.
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.
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
(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");
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;
}
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)
}
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;
}
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
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()
}
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;
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);
}
// 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);
}
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
}
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);
}
The concepts of ownership, borrowing, and slices ensure memory safety in Rust programs at compile time.
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
Posted on January 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.