The “Ownership” thing in Rust!

Tram Ho

Naturally, the company’s new project forced me to learn Rust, but the day before I first encountered Rust, I was already cursing in my stomach. Then the star push now forced me to learn Rust, frustrated. But if you read it over and over again, slowly, you will understand what is naturally like!

I see the problem that causes all other confusion in Rust is Ownership, data stored on the stack or stored on the heap.

1. Rust data types.

In Rust divided into 2 groups of data types, this paragraph is only a version, read the code and understand, too lazy to write.

1.1 Scalar

Scalar data types include:

  • Integer Types

arch means depending on the architecture of the computer running the program that it is 32-bits or 64-bits

  • Floating-Point Types include: f32 and f64 .
  • Boolean type: bool
  • Character type (Character type): char , this type of value is assigned only 1 character

  • Type &str : although this type is not fixed in size, its size must be determined at compile time.

Obviously from line 2 to line 3 the size of s has increased but Compiler still determines the size at each stage during compilation.

1.2 Compound

Compound data types include:

  • Tuple

  • Array

Note that arrays in rust won’t have any methods like pop or push to change the length of the array, or even implementing code like this:

Although when compile does not detect errors, but when running, it will give an error:

2. Stack and Heap

In Rust, memory is divided into two parts, Stack and Heap

  • Stack works in the following way: Last In First Out (push, pop), the data stored on the Stack must be data of known size at compile time and known-fixed size. during program runtime.
  • Heap is for data of unknown size and possible size while running the program (unknown size), then pointers to those data are stored on the Stack . The Heap works thanks to the Memory Allocator , every time a certain data wants to be stored on the Heap , Memory Allocator will find just enough area on the Heap to store the data in, and then push the pointer to the Stack .

So what data types are stored directly on the Stack? Including: integer types, floating-point types, char, &str, compound types whose members only include integer, floating-point, char, &str, these types when stored and retrieved are simply push Stack .

As for data types such as String , Vector , compound types whose members contain String or Vector , self-defined structs whose members contain String or Vector will be stored on the Heap .

This part is starting to hurt my head, like I just finished a cup of super concentrated Phuc Long tea.

The Three Rules of Ownership

  • Every value in Rust has an owner , no matter what data type the value is.
  • A value cannot have more than one owner at a time.
  • When the owner goes out of its scope, the value it is carrying will also be drop .

What is the scope of a variable?

For example:

If you read the code, you can imagine it, the scope of the variable s is determined by the nearest pair of curly braces {} that cover the declaration of the variable s , where s is the owner of the value "hello" and this value as said above, it is stored on the Stack .

3. Illustration Ownership

Again, in Rust, String and &str are two completely different data types, although both are for storing string literals, &str is a built-in Rust type and String is a code one must code to build it based on. Vector , and has a set of methods of its own.

Why is it necessary to need String when there is &str , as mentioned above, &str although it can change length when running the program, Compiler always determines its length at each passing time. compile process, and String will be for the values ​​that are entered by the user when the program runs, so Compile cannot know the length of that value in advance, so it must use String . For example:

String data will have the value stored on the Heap .

Normally when we declare a variable but then do nothing about it, when Compiler compiles it will show a warning = note: #[warn(unused_variables)] on by default , we can fix it by changing variable name to _s1 or add #![allow(unused)] if using std or #[allow(unused)] if not using std .

 

s1 will be stored in memory:

s1 is a data set stored on the Stack including ptr , len , capacity , ptr pointing to the actual value "hello" being stored on the Heap , this data set represents that s1 is the owner of the value "hello" .

completely different from the following case:

Since x is an i32 type, the size is known in advance and does not change the size, so the value of x will be stored entirely on the Stack , and x is the owner of that value.

Assignment (Shallow copy)

As a way to copy data on the Stack , consider the following example with String :

When we assign the value of s1 to s2 we are actually moving the ownership of the value "hello" on the Heap ( ownership ) from s1 to s2 , so since line 3, s1 has no value, so when printing it will show an error on the screen, Compiler will detect this error at compile time, the error is as follows:

Consider the following example with i32 :

This code is completely valid and runs normally, because i32 is a valid data type stored on the Stack .

The assignment aka Shallow copy will only copy the data on the Stack , which will perform the following different behaviors:

  • For single data types of known size such as interger , floating-point , &str , the value is stored on the Stack , so when performing the assignment, the value will be copied to another copy also on the Stack . does not violate any of Ownership ‘s principles, as in the example above, x is still valid after y is set to x .
  • For data types of unknown size such as String and Vector , the value is actually stored on the Heap and pointer len capacity is stored on the Stack , so when performing the assignment of the data on the stack , it will be copied as another copy on the stack. stack , but recalling Ownership ‘s 2nd rule is “A value cannot have more than one owner at a time.”, so for these data types, the assignment will be move ownership . from one variable to another. So with the String example above, when assigning s2 to s1 , s1 will no longer have any value, the command pritnln!("{}", s1) will get an error.

 

Clone (Deep copy)

The clone is a way of copying values ​​on the Heap , consider the following example:

Now s1 , s2 are 2 owner of 2 completely separate values ​​on the Heap , so s1 is still valid after performing let s2 = s1.clone()

Consider the example with i32 :

Although, the i32 data type stores the value on the Stack absolutely nothing on the Heap , but clone() still works, because it determines the Stack layer is the deepest layer of the i32 type, hence the clone() operation. clone() is exactly the same as assignment in this case.

Oke, I just added a bowl of pho with 2 tablespoons of MSG.

Ownership with Function

Consider the following example with String :

In the first line, we declare s as a variable of type String , s is now the owner of the value "hello" being stored on the Heap .

In the next line we call the function takes_ownership(s) with the parameter passed as s , looking at the declaration of the function as fn takes_ownership(some_string: String) String{...} , we can roughly understand the first one. assign some_string = s now the ownership of the value "hello" has been transferred from s to some_string , then run the code of the function with some_string , otherwise recalling the knowledge of the variable scope then the scope of some_string is only internal to the take_ownership function , at the end of this function some_string will be drop , so the command pritnln!("{}", s) will fail because s is empty.

Consider the example with i32 :

In the first line, it is still declared that x is the owner of the value 5 stored on the Stack .

In the next line, we call the function makes_copy(x) , with the input parameter x , roughly understood as the first assignment of some_integer = x , recalling the knowledge about the assignment with type i32 , then in fact x and some_integer is being 2 owner of 2 separate values ​​on Stack , next we execute the code of makes_copy() function for some_integer , the end of some_integer function will be drop , but due to ownership of x and some_integer completely located separate, so even if some_integer is drop , x is still valid. So the above code is completely error free.

How to preserve ownership of String when calling a function?

We simply give the return function an ownership :

The first will probably assign a_string = s1 , the ownership of "hello" is now a_string from s1 to move , but the return value of the function is now String , so when leaving the ownership function, it is move right from a_string returns s1 .

Why do we have to let s1 twice, because by default the variables in Rust are immutable , so s1 ‘s data is not allowed to be reassigned, so we have to choose to over-declare the variable s1 , to overcome this we can Edit the code as follows:

Simply add mut to the s1 declaration.

At this point, I just need to add a few lights, a little music, and I can go to the scene.

 

But it would be cumbersome if we had to return ownership every function along with the calculated value that we want the function to return, like the following example:

Fortunately, Rust has support for us References and Borrowing , which will give us access to the value of the variable without affecting the ownership of the variable. I’ll just introduce it for now, but wait until I’m excited to continue writing about this.

4. Summary

To summarize, there are a few ideas:

  • Data types with known sizes will be stored on the Stack
  • Unknown data types will be stored on the Heap
  • The assignment is Shallow copy , which only copies the value on the Stack. Performing an assignment with a data type of known size will create a new ownership separate from the old one, performing an assignment with a data type of unknown size will move ownership ownership the old variable to the new variable
  • The clone() operation is Deep copy , copies values ​​on the Heap , clone() can work with both known and unknown data types. How funny, how strange!
  • When a variable goes out of its scope, it is drop
  • To preserve an ownership when calling a function, that function either returns another ownership or the parameter passed to the function is in the form of reference .
Share the news now

Source : Viblo