[Learn Rust programming with Xiaojia] 4. Understand the concept of ownership in Rust

Series article directory

[Learn Rust programming with Xiaojia] 1. Basics of Rust programming
[Learn Rust programming with Xiaojia] 2. Use of Rust package management tools
[Learn Rust programming with Xiaojia] 3. The basic programming concept of Rust
[Learn Rust programming with Xiaojia] 4. Understand the concept of ownership in Rust

Article directory

  • Series Article Directory
  • foreword
  • 1. Ownership
    • 1.1. Ownership
    • 1.2, stack (Stack) and heap (Heap)
    • 1.3. Ownership Rules
    • 1.4, variable scope (Variable Scope)
    • 1.5, String type (String Type)
      • 1.5.1. String slice reference (&str type)
      • 1.5.2, String type
        • 1.5.2.1, Introduction to String
        • 1.5.2.2, create a String string
        • 1.5.2.2, append string
        • 1.5.2.3, insert string
        • 1.5.2.4, string replacement
        • 1.5.2.4, string deletion
        • 1.5.2.5, string concatenation
        • 1.5.2.6, use format! connection string
        • 1.5.2.7, the way of escaping `\`
        • 1.5.2.7, string line connection
        • 1.5.2.7, original string
        • 1.5.2.8, String with double quotes problem
        • 1.5.2.9, character array
    • 1.6, memory (Memory) and allocation (Allocation)
    • 1.7. How variables interact with data
      • 1.7.1. Move
      • 1.7.3, copy (copy)
      • 1.7.2, clone (clone)
    • 1.8. Ownership mechanism involving functions
    • 1.9. Ownership mechanism of function return value
  • 2. Reference and Borrowing
    • 2.1, reference (Reference)
    • 2.3. Borrowing
      • 2.3.1. Lease
      • 2.3.2、Mutable References
    • 2.4. Dangling Reference
  • 3. Slice Type
    • 3.1, String slice (String Slice)
    • 3.2. Array slicing
  • Summarize

Foreword

This chapter will explain a concept unique to Rust (ownership). Ownership is the most unique feature of Rust, which allows Rust to guarantee memory safety without requiring a garbage collector. So it’s important to understand how ownership works, and this chapter will explain the features related to ownership: borrowing, slicing, and how Rust lays out data in memory.

Main textbook reference “The Rust Programming Language”

1. Ownership

Ownership is the most unique feature of Rust, which allows Rust to guarantee memory safety without requiring a garbage collector.

1.1. Ownership

In programming languages such as Java there is a garbage collection mechanism that periodically looks for memory that is no longer used while the program is running, in C/C++ the programmer must explicitly allocate and free memory.

A third approach is used in Rust: memory is managed through an ownership system with a set of rules that the compiler checks, and if any rules are violated, the program will not compile. There are no features of ownership that slow down the program while it is running.

1.2, stack (Stack) and heap (Heap)

Many languages don’t need to think about the heap and the stack very often, but in a systems programming language like Rust, whether a value lives on the stack or the heap affects the behavior of the language and what you want to do with it.

Stacks are both thick sections that can be used at runtime, but they are structured differently. The stack stores values in the order they were acquired and removes values in the reverse order. This is known as last in first out. Adding data is called pushing, and deleting data is called popping.

All data stored on the stack must have a known fixed size. Data whose size is unknown at compile time or whose size may change must be stored on the heap.

The heap is poorly organized: when you put data on the heap, you request a certain amount of space. The memory allocator finds a large enough space in the heap, marks it as in use, and returns a pointer that is the address of that location. This process is called allocating on the heap, since the heap pointer to the heap is known Fixed size, so pointers can be stored on the stack.

1.3, Ownership Rules

Ownership has the following three rules

  • Every value in Rust has a variable called its owner;
  • There can only be one owner at a time;
  • When the owner is no longer in the scope of the program, the value will be deleted;

These three rules are the basis for the concept of ownership;

1.4, variable scope (Variable Scope)

By understanding the following code examples, you can understand the variable scope.

fn main() {<!-- -->
    let s1 = "hello";
    {<!-- --> // s2 is not valid here, it's not yet declared
        let s2 = "hello"; // s2 is valid from this point forward
        // do stuff with s2
    } // this scope is now over, and s is no longer valid
}

1.5, String Type

1.5.1, string slice reference (&str type)

A string type initialized with a string literal is a string of type &str. This type is of known length and is stored in the executable program’s read-only memory segment (rodata). Strings in rodata can be referenced by &str.

let s: &str = "hello";

If you want to use the str type directly, it is not allowed, you can only use it through Box.

1.5.2, String type

1.5.2.1, Introduction to String

At the language level, Rust has only one string type: str, which usually appears as a reference type &str, which is the string slice reference mentioned above. Although the language level only has the above-mentioned str type, there are many different types of string types in the standard library, among which the most widely used type is the String type.

The string in Rust is of Unicode type, so each character occupies 4 bytes of memory space, but the string is different, the string is UTF-8 encoding, that is, the number of bytes occupied by the character in the string is change.

String literal values are hardcoded into the program and they are immutable. But not all string values are actually known. If we want to take user input and store it, Rust provides a second string type. This type manages data allocated on the heap.

fn main() {<!-- -->
    let s1: &str = "hello";
    let s2:String = s1.to_string();
    let s3: &String = &s2;
    let s4: &str = &s2[0..3];
    let s5:String = String::from(s1);
}

It should be noted that rust requires that the index must be of type usize. If the start index is 0, it can be abbreviated as & amp;s[..3], and the end index can also be the last byte of String, then it can be abbreviated as & amp;s[ 1..], if you want to quote the whole String, it can be abbreviated as & amp;s[..].

String slice reference indices must be on unknown boundaries between characters, but since rust’s strings are utf-8 encoded, care must be taken.

1.5.2.2, create a String string

let s = "Hello".to_string();
let s = String::from("world");
let s: String = "also this".into();

1.5.2.2, append string

fn main() {<!-- -->
    let mut s = String::from("Hello ");
    s.push('r');
    println!("append character push() -> {}", s);

    s.push_str("ust!");
    println!("append string push_str() -> {}", s);
}

1.5.2.3, insert string

fn main() {<!-- -->
    let mut s = String::from("Hello rust!");
    s.insert(5, ',');
    println!("insert character insert() -> {}", s);
    s.insert_str(6, "I like");
    println!("insert string insert_str() -> {}", s);
}

1.5.2.4, string replacement

1. The replace method uses two types of strings;

fn main() {<!-- -->
    let string_replace = String::from("I like rust. Learning rust is my favorite!");
    let new_string_replace = string_replace.replace("rust", "RUST");
    dbg!(new_string_replace); // debug using macros
    let s = "12345";
    let new_s = s.replace("3", "t");
    dbg!(new_s);
}

2. The replace method uses two types of strings;

fn main() {<!-- -->
    let string_replace = "I like rust. Learning rust is my favorite!";
    let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
    dbg!(new_string_replacen);
}

3. replace_range only uses String type

fn main() {<!-- -->
    let mut string_replace_range = String::from("I like rust!");
    string_replace_range.replace_range(7..8, "R");
    dbg!(string_replace_range);
}

1.5.2.4, string deletion

1. pop

fn main() {<!-- -->
    let mut string_pop = String::from("rust pop Chinese!");
    let p1 = string_pop. pop();
    let p2 = string_pop. pop();
    dbg!(p1);
    dbg!(p2);
    dbg!(string_pop);
}

2. remove

fn main() {<!-- -->
    let mut string_remove = String::from("Test remove method");
    println!(
        "string_remove takes {} bytes",
        std::mem::size_of_val(string_remove. as_str())
    );
    // delete the first Chinese character
    string_remove. remove(0);
    // The following code produces an error
    // string_remove. remove(1);
    // directly delete the second Chinese character
    // string_remove. remove(3);
    dbg!(string_remove);
}

3. truncate

fn main() {<!-- -->
    let mut string_truncate = String::from("Test truncate");
    string_truncate.truncate(3);
    dbg!(string_truncate);
}

4. Clear

fn main() {<!-- -->
    let mut string_clear = String::from("string clear");
    string_clear.clear(); // equivalent to string_clear.truncate(0)
    dbg!(string_clear);
}

1.5.2.5, string concatenation

String concatenation uses the + or + = operator, requiring that the parameter on the right must be a string slice reference. Using + is equivalent to using the add method in the std::string standard library

fn main() {<!-- -->
    let string_append = String::from("hello ");
    let string_rust = String::from("rust");
    // // &string_rust will be automatically dereferenced as &str, because of the deref coercing feature. This feature allows the incoming &String to be converted to &str before the API is executed.
    let result = string_append + & string_rust;
    let mut result = result + "!";
    result + = "!!!";

    println!("Connection string + -> {}", result);
}

1.5.2.6, use format! connection string

This method is applicable to String and &str, similar to the sprintf function provided by C/C++

fn main() {<!-- -->
    let s1 = "hello";
    let s2 = String::from("rust");
    let s = format!("{} {}!", s1, s2);
    println!("{}", s);
}

1.5.2.7, the way of escaping \

fn main() {<!-- -->
    // Through the hexadecimal representation of \ + characters, escape and output a character
    let byte_escape = "I'm writing \x52\x75\x73\x74!";
    println!("What are you doing\x3F (\x3F means ?) {}", byte_escape);

    // \u can output a unicode character
    let unicode_codepoint = "\u{211D}";
    let character_name = ""DOUBLE-STRUCK CAPITAL R"";

    println!(
        "Unicode character {} (U+211D) is called {}",
        unicode_codepoint, character_name
    );
}

1.5.2.7, string line connection

fn main() {<!-- -->
    let long_string = "String literals
                    can span multiple lines.
                    The linebreak and indentation here \
                    can be escaped too!";
    println!("{}", long_string);
}

1.5.2.7, raw string

Strings starting with r are not escaped

fn main() {<!-- -->
    println!("{}", "hello \x52\x75\x73\x74"); // output hello Rust
    let raw_str = r"Escapes don't work here: \x3F \u{211D}"; // raw string
    println!("{}", raw_str); // output Escapes don't work here: \x3F \u{211D}
}

1.5.2.8, string with double quotes

Rust provides the r# way to avoid the problem of nested quotes.

fn main() {<!-- -->
    // If the string contains double quotes, you can add # at the beginning and end
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // If there is still ambiguity, you can continue to increase #, no limit
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
}

1.5.2.9, character array

Since the string of rust is utf-8 encoded, the String type does not allow indexing in units of characters. So String provides chars() to traverse characters and bytes() to traverse bytes.

But it is more difficult to get the substring from String, and there is no related method provided in the standard library.

1.6, memory (Memory) and allocation (Allocation)

Character output literals are fast and efficient because they are hardcoded into the final executable. These characters are immutable. But we can’t fit in a memory block of the binary for every piece of text whose size is unknown at compile time and may change when the program is run.

In order for the String type to support variable and growable text fragments, we need to allocate a certain amount of memory on the heap to store the content, which means that the memory must be requested from the memory allocator at runtime, and we need a way, in Return this memory to the allocator when you are done using the String.

Part 1: When String::from is called, its implementation requests the memory it needs.
Part 2: In languages with Garbage Collector (GC), GC will clean up unused memory, we don’t need to think about it. In languages without GC it is our responsibility to recognize that the memory is no longer in use, and for the calling code to release it explicitly.

Rust takes a different approach: once the variable that owns the memory goes out of scope, the memory is returned automatically (Rust calls a special function called drop for us).
In C++, this pattern of releasing resources at the end of the project’s lifetime is sometimes called resource acquisition as initialization (RALL).

1.7, the way variables interact with data

1.7.1, Move

1. Assignment

Assigning one variable to another transfers ownership.

 let s1 = String::from("hello");
    let s2 = s1;

2. Parameter passing or function return

Assignment is not the only operation that involves movement, values are also moved when passed as parameters or returned from functions.

3. Assign to structure or enum

1.7.3, copy (copy)

Integer types of known size at compile time live entirely on the stack, so the actual value can be copied directly and quickly.

 let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);

In Rust there is a special annotation called the Copy trait that we can put on types that are stored on the stack, just like integers, if a type implements the Copy trait then its variables are not moved.

Rust won’t allow us to annotate a type with Copy if the type or any part of it implements the Drop trait.

Types that implement the Copy trait

  • all integer types

  • boolType: true, false

  • Floating point type; f32, f64

  • Character type: char

  • Tuple (only contains the Copy trait), for example (i32, i32) is copy, and (i32, String) is move;

1.7.2, clone

 let s1 = String::from("hello");
    let s2 = s1. clone();

    println!("s1 = {}, s2 = {}", s1, s2);

1.8. Ownership mechanism involving functions

fn main() {<!-- -->
    let s = String::from("hello"); // s comes into scope

    takes_ownership(s); // s's value moves into the function...
                                    // ... and so is no longer valid here

    println!("s {}", s);
    let x = 5; // x comes into scope

    makes_copy(x); // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) {<!-- --> // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) {<!-- --> // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

1.9. Ownership mechanism of function return value

fn main() {<!-- -->
  let s1 = gives_ownership(); // gives_ownership moves its return
                                      // value into s1

  let s2 = String::from("hello"); // s2 comes into scope

  let s3 = takes_and_gives_back(s2); // s2 is moved into
                                      // takes_and_gives_back, which also
                                      // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {<!-- --> // gives_ownership will move its
                                           // return value into the function
                                           // that calls it

  let some_string = String::from("yours"); // some_string comes into scope

  some_string // some_string is returned and
                                           // moves out to the calling
                                           // function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String {<!-- --> // a_string comes into
                                                    // scope

  a_string // a_string is returned and moves out to the calling function
}

2. Reference and Borrowing

2.1, Reference

Reference (Reference) is a concept familiar to C++ developers. If you are familiar with the concept of pointers, you can regard it as a pointer. Essentially, references are indirect access to variables.

We can use references to avoid the problem that the original variable cannot be used due to the transfer of ownership.

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.len()
}

Referenced schematic diagram
Because s is a reference to String, it has no ownership and will not be dropped when the function ends.

2.3, Borrowing

2.3.1, lease

References do not acquire ownership of the value, references can only borrow ownership of the value. A reference is itself a type and has a value that records the location of another value, but a reference does not take ownership of all values.

fn main() {<!-- -->
    let s = String::from("hello");

    change( & s);
}

fn change(some_string: &String) {<!-- -->
    some_string.push_str(", world");
}
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a ` & amp;` reference
 --> src/main.rs:8:3
  |
8 | some_string. push_str(", world");
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a ` & amp;` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {<!-- -->
  | ~~~~~~~~~~~

For more information about this error, try `rustc --explain E0596`.
error: could not compile `hello` (bin "hello") due to previous error

From the above error message, we can know that reference is a leased reference, which can only be read but not written. We can use & amp;mut to make variable references.

2.3.2, Mutable References

Use Mutable References to modify the referenced content.

fn main() {<!-- -->
  let mut s = String::from("hello");

  change( &mut s);
}

fn change(some_string: & amp;mut String) {<!-- -->
  some_string.push_str(", world");
}

A variable can have only one Mutable References.

When there are immutable references to the same value, there cannot be mutable references.

2.4, Dangling Reference

Dangling references seem to be also called wild pointers.

If there is a programming language with the concept of pointers, it refers to the kind of data and pointers that do not actually point to a real accessible data (note that it is not necessarily a null pointer, but may also be a released resource), they are like losing suspension The string of objects, so called dangling references.

Dangling references are not allowed in the Rust language, if there is, the compiler will find it

fn main() {<!-- -->
    let reference_to_nothing = dangle();
}

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

     &s
}

Obviously, with the end of the dangle function, the value of its local variable itself is not used as a return value, and is released. But its reference is returned, and the value pointed to by this reference can no longer be determined, so it is not allowed to appear.

error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {<!-- -->
  | ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> & amp;'static String {<!-- -->
  | + + + + + + +

For more information about this error, try `rustc --explain E0106`.
error: could not compile `hello` (bin "hello") due to previous error

3. Slice Type

Slices are partial references to data values.

3.1, String slice (String Slice)

The simplest and most commonly used data slice type is the string slice (String Slice).

3.2, array slice

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);

Summary

That’s all for today

  • This article introduces the ownership, slicing, character output type, lease, and variable reference of rust. The content of this chapter is relatively difficult to understand;