Rust Ownership
Computer programs must manage the memory resources they use at runtime.
Most programming languages have memory management capabilities:
Languages like C/C++ manage memory primarily through manual means; developers need to manually allocate and free memory resources. However, to improve development efficiency, as long as it doesn't affect program functionality, many developers don't have the habit of freeing memory in a timely manner. Therefore, manual memory management often leads to resource waste.
Programs written in Java run in a virtual machine (JVM), which has the ability to automatically reclaim memory resources. However, this approach often reduces runtime efficiency, so the JVM reclaims resources as little as possible, which also causes programs to occupy a larger amount of memory resources.
Ownership is a novel concept for most developers; it is a syntactic mechanism designed by the Rust language for efficient memory use. The ownership concept was created to allow Rust to analyze the usefulness of memory resources at compile time more effectively, thereby achieving memory management.
Ownership Rules
Ownership has the following three rules:
- Every value in Rust has a variable, called its owner.
- There can be only one owner at a time.
- When the owner goes out of scope, the value will be dropped.
These three rules are the foundation of the ownership concept.
Next, we will introduce concepts related to the ownership concept.
Variable Scope
We use the following program to describe the concept of variable scope:
{
// 在声明以前,变量 s 无效
let s = "example";
// 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效Variable scope is an attribute of a variable. It represents the valid region of the variable; by default, it is valid from the declaration of the variable until the end of the scope in which the variable resides.
Memory and Allocation
If we define a variable and assign a value to it, the variable's value resides in memory. This is very common. But if the length of the data we need to store is uncertain (for example, a string entered by the user), we cannot determine the data length at definition time, nor can we make the program allocate a fixed-length memory space for data storage at the compilation stage. (Some say allocating as much space as possible can solve the problem, but this method is not elegant.) This requires a mechanism by which the program itself requests memory during runtime—the heap. All "memory resources" discussed in this chapter refer to the memory space occupied by the heap.
With allocation comes deallocation; a program cannot keep occupying a memory resource forever. Therefore, the key factor determining whether resources are wasted is whether they are deallocated in a timely manner.
Let's write an equivalent C version of the string example program:
char *s = strdup("example");
free(s); // Deallocate the resource of s
}
Obviously, Rust does not call a free function to release the resources of string s (I know this is an incorrect way to write it in C, because "example" is not on the heap; here we assume it is). The reason Rust does not explicitly show the deallocation step is that when the variable's scope ends, the Rust compiler automatically adds a step to call the resource deallocation function.
This mechanism may seem simple: it just helps the programmer add a resource deallocation function call at the appropriate place. But this simple mechanism can effectively solve one of the most troublesome programming problems in history for programmers.
Ways Variables and Data Interact
There are two main ways variables and data interact: Move and Clone.
Move
Multiple variables can interact with the same data in different ways in Rust:
let x = 5; let y = x;
This program binds the value 5 to variable x, then copies x's value and assigns it to variable y. Now there will be two values 5 on the stack. In this case, the data is of the "basic data" type, which doesn't need to be stored on the heap. The "move" for data only on the stack is direct copying, which doesn't take more time or more storage space. The "basic data" types are:
- All integer types, such as i32, u32, i64, etc.
- The Boolean type bool, with values true or false.
- All floating-point types, f32 and f64.
- The character type char.
- Tuples containing only data of the above types.
But if the interacting data is on the heap, it's a different situation:
let s1 = String::from("hello");
let s2 = s1;The first step creates a String object with the value "hello". Here, "hello" can be considered data of uncertain length that needs to be stored on the heap.
The second step is slightly different (This is not entirely true; it's only for comparison and reference):

As shown in the figure: the two String objects are on the stack, each String object has a pointer to the "hello" string on the heap. When assigning to s2, only the data on the stack is copied; the string on the heap remains the original string.
We mentioned earlier that when a variable goes out of scope, Rust automatically calls the resource deallocation function and cleans up the variable's heap memory. But if both s1 and s2 are deallocated, the "hello" in the heap area would be deallocated twice, which is not allowed by the system. To ensure safety, s1 becomes invalid when assigning to s2. That's right: after assigning s1's value to s2, s1 can no longer be used. The following program is wrong:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 错误!s1 已经失效
So the actual situation is:

s1 exists in name only.
Clone
Rust tries to reduce program runtime costs as much as possible, so by default, larger data is stored on the heap, and data interaction uses the move method. But if you need to simply copy data for other uses, you can use the second way of data interaction—cloning.
Example
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
Output:
s1 = hello, s2 = hello
Here, the "hello" on the heap is indeed copied, so s1 and s2 are each bound to a value, and when deallocated, they are treated as two resources.
Of course, cloning should only be used when copying is needed, after all, copying data takes more time.
Ownership Mechanism in Functions
This is the most complex situation for variables.
If a variable is passed as a function argument to another function, how do you safely handle ownership?
The following program describes how the ownership mechanism works in this situation:
Example
let s = String::from("hello");
// s is declared valid
takes_ownership(s);
// s's value is passed as an argument into the function
// So s can be considered moved, and it is no longer valid from here
let x = 5;
// x is declared valid
makes_copy(x);
// x's value is passed as an argument into the function
// But x is a basic type, so it remains valid
// Here we can still use x but not s
} // Function ends, x becomes invalid, then s. But s has been moved, so it doesn't need to be deallocated
fn takes_ownership(some_string: String) {
// A String argument some_string is passed in, valid
println!("{}", some_string);
} // Function ends, argument some_string is deallocated here
fn makes_copy(some_integer: i32) {
// An i32 argument some_integer is passed in, valid
println!("{}", some_integer);
} // Function ends, argument some_integer is a basic type, no need to deallocate
If a variable is passed as an argument to a function, it has the same effect as a move.
Ownership Mechanism of Function Return Values
Example
let s1 = gives_ownership();
// gives_ownership moves its return value to s1
let s2 = String::from("hello");
// s2 is declared valid
let s3 = takes_and_gives_back(s2);
// s2 is moved as an argument, s3 gains ownership of the return value
} // s3 is invalid and deallocated, s2 is moved, s1 is invalid and deallocated.
fn gives_ownership() -> String {
let some_string = String::from("hello");
// some_string is declared valid
return some_string;
// some_string is moved out of the function as a return value
}
fn takes_and_gives_back(a_string: String) -> String {
// a_string is declared valid
a_string // a_string is moved out of the function as a return value
}
The ownership of a variable used as a function return value is moved out of the function and back to the calling location, rather than being invalidated and deallocated directly.
References and Borrowing
Reference is a concept familiar to C++ developers.
If you are familiar with pointers, you can think of it as a kind of pointer.
In essence, a "reference" is an indirect way to access a variable.
Example
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2);
}
Output:
s1 is hello, s2 is hello
&The operator can take a "reference" to a variable.
When a variable's value is referenced, the variable itself is not considered invalid. This is because a "reference" does not copy the variable's value on the stack:

The same principle applies to function parameter passing:
Example
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()
}
Output:
The length of 'hello' is 5.
References do not gain ownership of a value.
References can only borrow the ownership of a value.
A reference itself is also a type and has a value. This value records the location of another value, but the reference does not own the referenced value:
Example
let s1 = String::from("hello");
let s2 = &s1;
let s3 = s1;
println!("{}", s2);
}
This program is incorrect: because s1, which s2 borrowed, has already moved its ownership to s3, s2 will no longer be able to continue borrowing s1's ownership. If s2 needs to use that value, it must re-borrow:
Example
let s1 = String::from("hello");
let mut s2 = &s1;
let s3 = s1;
s2 = &s3; // Re-borrow ownership from s3
println!("{}", s2);
}
This program is correct.
Since references do not have ownership, even if they borrow ownership, they only enjoy usage rights (this is the same as renting a house).
If you attempt to use the borrowed right to modify data, it will be prevented:
Example
let s1 = String::from("run");
let s2 = &s1;
println!("{}", s2);
s2.push_str("oob"); // Error: modifying the borrowed value is prohibited
println!("{}", s2);
}
In this program, s2's attempt to modify s1's value is prevented; borrowed ownership cannot modify the owner's value.
Of course, there is also a mutable borrowing method. Just like when you rent a house, if the property regulations allow the owner to modify the house's structure, and the owner also declares in the contract that you are granted this right during the rental, you can renovate the house:
Example
let mut s1 = String::from("run");
// s1 is mutable
let s2 = &mut s1;
// s2 is a mutable reference
s2.push_str("oob");
println!("{}", s2);
}
This program is fine. We use `&mut` to denote mutable reference types.
Compared with immutable references, besides having different permissions, mutable references do not allow multiple references, while immutable references do:
Example
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
This program is incorrect because s has multiple mutable references.
Rust's design of mutable references is mainly motivated by considerations of data access collisions under concurrency, preventing such things from happening at the compilation stage.
Since one of the necessary conditions for a data access collision is that the data is written by at least one user and simultaneously read or written by at least one other user, a value is not allowed to be referenced by any other reference while it is mutably referenced.
Dangling References
This is a concept with a different name. In programming languages that have the concept of pointers, it refers to a pointer that does not actually point to truly accessible data (note: it is not necessarily a null pointer; it could also be a resource that has already been released). They are like ropes that have lost the object they are hanging, hence the name "dangling reference".
"Dangling references" are not allowed in Rust; if they exist, the compiler will detect them.
Below is a typical example of a dangling reference:
Example
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Obviously, as the `dangle` function ends, the value of its local variable is not used as the return value and is released. However, its reference is returned instead. The value pointed to by this reference can no longer be confirmed to exist, so this is not allowed.
Other Extensions