Lv777

Lv777

github
twitter
jike
playstation
xiaoyuzhou

Ownership

Torture, I wrote half of the programming concept notes on my work computer and forgot to push them. Using the git plugin in Obsidian, I don't like automatic backups, so I have to remember to manually sync every time I finish writing.

When I first read the Bible, I realized that ownership is the source of a series of complex designs. At that time, I planned to read half of it and then read the ownership again. Of course, I eventually chose to finish it all at once. I have tried to learn Rust several times before, but I always stopped at ownership. Now, when I read it again, it feels a bit familiar.

What is Ownership#

For front-end developers who are used to high-level language JS, they don't need to consider memory management in their daily work except before interviews, because JS has GC (Garbage Collection) - the garbage collection often mentioned in interviews, which automatically scans and clears unused memory at regular intervals. In traditional low-level languages like C, when writing code, you need to allocate and deallocate memory yourself in addition to considering the logical implementation. However, Rust introduces a new way to manage memory, which is ownership.

Why Ownership is Needed#

Low-level languages need to consider performance and code size. Although GC is convenient, it needs to run with the program. Despite the optimizations made by the engine, it cannot bypass the scanning and analysis at runtime, which can be considered as sacrificing performance for convenience1. Allowing developers to directly manage memory can easily lead to memory leaks or null pointer crashes due to human errors. Rust focuses on memory safety, relying on ownership. I understand ownership as an abstract constraint. Developers write code according to specific patterns, making it easier for the compiler to statically analyze and insert memory management content. It feels like managing memory, but not completely under your control. In addition, the ownership model helps developers better restrict behavior and understand code, avoiding common errors and reducing unnecessary memory allocation and copying to improve performance.

Heap and Stack will not be discussed here. This section helped me recall a lot of forgotten knowledge.

How to Use#

Just fix the compilation errors 🤪

To restate the rules: at any given time, a value (character, number, object, etc.) can only have one owner (variable), and the value will be dropped when the owner goes out of scope.

Understanding#

In order to avoid vulnerabilities caused by double freeing of memory, Rust only allows one pointer to point to memory on the heap, and does not allow behaviors similar to shallow copy in JS. By default, values are moved when passed, which means ownership is transferred. Additionally, you can use the clone method to consume resources and achieve deep copy. For simple data that exists on the stack, you can implement the Copy trait to achieve fast copying without considering ownership.

Capacity#

When discussing the memory allocation of String, it mentioned the content stored on the stack, the pointer ptr, len, and capacity. In the Vec documentation, you can find the difference between the two:

The capacity of a Vector is the amount of space allocated for any future elements that will be added onto the Vector. This is not to be confused with the length of the Vector, which specifies the number of actual elements within the vector. If a vector's length exceeds its capacity, its capacity will automatically increase, but its elements will have to be reallocated.
For example, a Vector that has a capacity of 10 and a length of 0 is an empty vector, and it can store up to 10 more elements. Pushing 10 or fewer elements into it will not change its capacity or cause reallocation. However, if the vector's length grows to 11, it will have to reallocate, which can be slow. For this reason, it is recommended to use Vec::with_capacity to specify the expected size of the vector.

References and Borrowing#

Because of the ownership mechanism, reusing a value becomes complicated, especially when ownership is passed into a function, and you need to return ownership from the function in order to reuse it. You can use the & keyword to create a reference to a value and avoid repetitive passing of ownership and returning. A reference is a pointer to a value, and the ownership of the value itself is not transferred. The behavior of creating a reference is called borrowing.

Holding#

The concept I made up This seems to be the concept of lifetimes.

References can also be mutable or immutable, and there is an additional restriction that if you hold a mutable reference, you cannot hold other mutable or immutable references at the same time. Holding means that during the period from creation to the last use, whether created first or later, mutable references and other references cannot be interleaved (a bit abstract). This restriction is to avoid data races. If a reference is not used after creation in the same scope, there is no problem with data races. Similarly, holding multiple immutable references is allowed. Rust has a lot of restrictions on mutability.

let str = String::from("x");
let s1 = &mut str;
// During this period, s1 is held and no other references can exist
println!("{s1}")

References may cause dangling pointers, which means the reference is still there, but the memory it points to is no longer there or is not the same as when the reference was created. Rust avoids this situation by using [[lifetimes]]. It feels like constantly patching up, borrowing to make up for the inconvenience caused by ownership, and lifetimes to make up for the pitfalls caused by borrowing.

Taking References and Taking Addresses#

Lv777: In Rust, does &v[0] mean a reference to the element at index 0 of v, or a reference to the index 0 element of v?


Raycast AI: In Rust, &v[0] represents a reference to the element at index 0 of the vector v. Specifically, v is a vector, and &v[0] is an immutable reference to the first element of the vector. The type of this expression is &T, where T is the type of the elements in the vector. Therefore, you can access the value of this element by using *(&v[0]) or v[0].


Lv777: The precedence of the & keyword


Raycast AI: In Rust, the & symbol has two different uses, taking a reference and taking an address. They have different precedences.
The precedence of taking a reference is higher than taking an address. This means that if you write code like &x.foo(), it will be parsed as &(x.foo()), not (&x).foo().
If you want to take the address of a variable and convert it to a pointer type, you can use the as keyword, which has a lower precedence than the & symbol. For example, &x as *const T converts the address of x to a constant pointer to type T.


Lv777: What are taking references and taking addresses?


Raycast AI: In Rust, the & symbol has two different uses, taking a reference and taking an address.
Taking a reference means creating an immutable reference to a value using the & symbol. An immutable reference allows you to read the value but not modify it. For example, if x is an integer, then &x will create an immutable reference to x. You can use the * symbol to dereference the reference and access the value, such as *(&x) or x.
Taking an address means getting the memory address of a variable using the & symbol. This is typically used to pass the address of a variable to a function or other code so that they can access the value of the variable. For example, if x is an integer, then &x will return the memory address of x. You can convert this address to a pointer type, such as &x as *const i32 or &x as *mut i32, and pass it to other code.
It is important to note that taking a reference and taking an address are different operations and they return different types. Taking a reference returns a reference to a value, with the type &T, where T is the type of the value. Taking an address returns a pointer to a variable, with the type *const T or *mut T, where T is the type of the variable.

Slice#

I didn't pay attention to this concept when I first read it, and then I forgot which chapter it suddenly came up with and I couldn't understand it.

A slice is a reference to a part of a value. It is declared using a Range to specify the starting and ending indices. The starting index is the first position to be included in the slice, and the ending index is the position after the last element to be included in the slice.

let str = String::from("123456")
let sli = str[1..3]; // 23
let all = str[..]; // The starting index is 0, or the ending index is len, so it can be omitted

I have doubts about the ending index, let's see why-is-slice-end-index-logic-as-it-is.

As mentioned in [[#Capacity]], String stores its content on the stack, and the pointer points to the starting position of the data on the heap. Slice is similar, its pointer points to the starting index declared when creating the slice, and it does not have capacity. len is calculated by subtracting the starting index from the ending index declared when creating the slice. Slice is a special immutable reference to a value and is subject to the restrictions of [[#Holding]].

The documentation uses the use case of String to introduce the convenience of Slice, which is indeed difficult to describe. At first, I had difficulty understanding it because I was applying my understanding of strings in JS, but String in Rust is not a scalar and cannot be treated with simple Copy thinking.

Footnotes#

  1. https://zackoverflow.dev/writing/unsafe-rust-vs-zig/

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.