本节介绍Rust中最复杂的内容之一------所有权。
多数语言只要掌握基本语法和语句,就可以流畅地写出代码,但Rust不是,只有掌握了所有权才算真正入门。
rust的学习曲线
前言
所有权(Ownership)是 Rust 最独特、最核心的特性,它使得 Rust 能够在没有垃圾回收(GC)的情况下保证内存安全。所有权系统基于三个核心规则,在编译时进行检查。
Rust的所有权规则基于三个基本原则:
- 每个值都有一个所有者:变量绑定到一个值时,这个变量就是它的所有者。
- 值只有一个所有者:不能同时有多个所有者,这防止了数据竞争。
- 所有者超出作用域时,值会被丢弃:这自动释放内存。
为什么Rust引入了所有权系统
在 Rust 出现之前,许多系统级编程语言(如 C 和 C++)依赖手动内存管理,而 Java 或 JS 使用垃圾回收(GC)来自动管理内存,前者容易导致诸多内存错误:内存泄漏(忘记释放)、悬垂指针(dangling pointers,使用已释放的内存)、双重释放(double free)、缓冲区溢出(buffer overflows);而后者运行时会引入额外的开销,而且也可能造成内存泄漏(比如js中的闭包问题)。
既不需要开发者手动管理内存,又没有引入GC,那么如何确定一个值已经使用完成可以清理呢?最容易想到的两点就是
- 没有指针指向这个值------这引申出来的就是所有者和引用计数
- 值的作用域结束------这引申出来的就是Rust中的生命周期
前置知识:堆和栈
大多数编程语言使用时都不需要关心堆和栈的概念,但Rust中,理解堆和栈才能更容易掌握所有权。
在计算机内存管理中,堆(Heap)和栈(Stack)是两种常用的内存区域,用于存储程序运行时的数据。它们在内存分配、管理和使用方式上有很大的不同。
栈(Stack)
特点:
- 快速分配和释放:栈内存的分配和释放通过移动栈指针实现,速度非常快。
- 后进先出(LIFO) :栈是一种后进先出的数据结构,最后放入的数据最先被取出。
- 固定大小:栈的大小通常有限,由操作系统或编译器决定,可能会溢出。
- 局部变量和函数调用:栈用于存储局部变量、函数参数和返回地址等。
- 自动管理:栈内存的分配和释放由编译器自动管理,无需程序员干预。
堆(Heap)
特点:
- 动态分配:堆内存可以在运行时动态分配,大小可变。
- 手动管理:在有些语言(如C/C++)中需要手动管理堆内存的分配和释放,但Rust通过所有权系统自动管理。
- 较大空间:堆通常比栈大得多,受限于系统可用内存。
- 访问速度较慢:堆内存的分配和释放需要更复杂的管理,速度相对较慢。
- 无特定顺序:堆内存可以以任意顺序分配和释放,因此会产生内存碎片。
堆的工作原理:
- 程序在运行时通过分配器(Allocator)请求堆内存。
- 分配器在堆中找到一块足够大的未使用内存,将其标记为已使用,并返回指向该内存的指针。
- 当不再需要时,内存必须被释放,以便重用。
Rust中存放在栈中的值,一定是能在编译器确定其大小,不涉及堆分配,并且通常都实现了Copy。常见的类型有数字,bool,char,单元,数组/元组(结构本身),而像String,Vec这些类型,其结构本身和指针存放在栈上,值存放在堆上。
Rust所有权
Rust所有权三原则
所有权的核心理念就是以下三个原则:
- 每个值都有一个所有者
- 值只有一个所有者
- 所有者超出作用域时,值会被丢弃
每个值都有一个所有者
可以把值(比如一个字符串、数字、结构体实例)想象成一个物品,这个物品在任何时刻都归且仅归一个变量(所有者)所有。
rust
fn main() {
// 变量 s 是字符串 "hello" 的所有者
let s1 = String::from("hello");
// 此时只有 s 能操作这个字符串,它是唯一的所有者
println!("{}", s1);
}
值只有一个所有者
这个规则也叫 "移动(Move)语义"------ 如果把值从一个变量转移给另一个变量,原变量会立刻失去所有权,不能再使用(相当于物品从原主人手里转移给新主人,原主人再也无权支配)。
rust
let s1 = String::from("hello");
// 此时只有 s 能操作这个字符串,它是唯一的所有者
println!("{}", s1);
// 将 s1 的所有权转移给 s2(移动)
let s2 = s1;
// 错误!s1 已经失去所有权,编译器会报 错borrow of moved value: `s1`
// println!("{}", s1);
// 正确:s2 是当前唯一的所有者
println!("{}", s2);
所有者超出作用域时,值会被丢弃(销毁)
作用域是一个项(item)在程序中有效的范围。假设有这样一个变量:
rust
let s = "hello";
变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前作用域 结束时都是有效的。下面代码标明了变量 s 在何处是有效的。
rust
// s 在这里无效,它尚未声明
// print!("{}", s); //cannot find value `s` in this scope
{
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
println!("{}", s);
} // 此作用域已结束,s 不再有效
// print!("{}", s) //cannot find value `s` in this scope
- 当
s进入作用域时,它就是有效的。 - 这一直持续到它离开作用域为止。
- 当离开作用域时,Rust 会自动调用
drop函数释放该值占用的内存
反例:一个值能有两个所有者?
下面这个例子中似乎违背了第二条原则------值只有一个所有者。
rust
let x = 5;
let y = x; // move?
println!("x = {}, y = {}", x, y); // 两者都有效
这就要展开说起所有权的操作了。
所有权操作
在Rust中,所有权(Ownership)密切相关的主要操作有:copy(复制)、move(移动)、和clone(克隆)。
1. Copy
Copy是一个trait,表示类型可以通过简单的位复制(bitwise copy)来创建新值,而不会使原变量失效。如果一个类型实现了Copy trait,那么赋值操作或传参时就会复制值,而不是移动。这意味着原变量仍然有效。
基本标量类型(如整数、浮点数、布尔值、字符)以及由这些类型组成的元组和数组通常实现了Copy。
rust
let x = 5;
let y = x; // 复制值,因为 i32 实现了 Copy trait
println!("x = {}, y = {}", x, y); // 两者都有效
2. Move
当将一个值赋给另一个变量时,或者将值传递给函数时,如果该类型没有实现Copy trait,那么默认会发生移动(move)。移动意味着原变量不再拥有该值的所有权,所有权转移到新的变量或函数中。移动后,原变量将无法再被使用,因为所有权已经转移。
rust
let s1 = String::from("hello");
let s2 = s1; // 所有权从 s1 移动到 s2
// println!("{}", s1); // 编译错误!s1 已失效
println!("{}", s2); // 正确:hello
内存布局变化:
text
之前:
s1 -> 堆上的 "hello"
之后:
s2 -> 堆上的 "hello"
s1 -> 无效(已被移动)
3. 克隆(Clone)
Clone trait允许我们显式地创建一个值的深拷贝(deep copy)。对于实现了Clone的类型,我们可以调用clone方法来创建一个完全独立的新值,原变量和新变量彼此独立,修改一个不会影响另一个。
例如,String类型实现了Clone,所以我们可以克隆一个字符串:
rust
let s1 = String::from("hello");
let s2 = s1.clone(); // 深度复制,创建新的堆分配
println!("s1 = {}, s2 = {}", s1, s2); // 两者都有效
如果能理解以上内容,那么就能理解函数中的所有权操作
函数与所有权
1. 参数传递移动所有权
rust
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权移动到函数内
// println!("{}", s); // 错误!s 不再有效
let x = 5;
makes_copy(x); // x 被复制,仍然有效
println!("x = {}", x); // 正确
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,调用 drop,内存被释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,无事发生
2. 返回值转移所有权
rust
fn test2() {
let s1 = String::from("hello");
let s2 = takes_and_gives_back(s1);
// print!("{}", s1);//错误:borrow of moved value: `s1
print!("{}", s2);
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 返回,所有权转移给调用者
}
附:切片(Slices)与所有权
切片是对集合中一段连续元素的引用,不获取所有权。
rust
let s = String::from("hello world");
let hello = &s[0..5]; // 对 s 的一部分的引用
let world = &s[6..11]; // 另一个引用
let whole = &s[..]; // 对整个字符串的引用
// s 仍然有效,因为切片只是引用
let a = "hello";
let b = a; // copy,Shared references (`&T`)都实现了copy
println!("{},{}", a, b)
借用(Borrowing)
为了避免频繁的所有权转移,Rust 提供了引用(references)的概念,称为"借用"。
1. 不可变引用(&)
rust
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 借用,不获取所有权
println!("'{}' 的长度是 {}", s1, len); // s1 仍然有效
}
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // s 离开作用域,但因为它不拥有所有权,所以不会丢弃任何东西
2. 可变引用(&mut)
rust
fn main() {
let mut s = String::from("hello");
change(&mut s); // 可变引用
println!("{}", s); // hello, world!
}
fn change(some_string: &mut String) {
some_string.push_str(", world!");
}
借用规则(编译时检查)
- 规则 1 :任意时刻,只能有一个可变引用 或 多个不可变引用
- 规则 2:引用必须总是有效的
rust
let mut s = String::from("hello");
let r1 = &s; // ✓ 不可变引用
let r2 = &s; // ✓ 另一个不可变引用
// let r3 = &mut s; // ✗ 不能同时有可变和不可变引用 cannot borrow `s` as mutable because it is also borrowed as immutable
println!("{}, {}", r1, r2); // 使用完毕(后续代码不再使用r1,r2)
let r3 = &mut s; // ✓ 现在可以创建可变引用
r3.push_str(", world");
解引用
在 Rust 中,引用(&T)允许你借用值而不获取所有权。要访问引用指向的值,需要使用解引用操作符 *。
*解引用
rust
let x = 5;
let y = &x; // y 是对 x 的引用
assert_eq!(5, x);
assert_eq!(5, *y); // 解引用 y 得到值 5
这里 *y 就是解引用操作,它返回 y 所指向的值。
智能指针与 Deref Trait
Rust 中的智能指针(如 Box<T>、Rc<T>、Arc<T>)通过实现 Deref trait 来获得类似引用的行为。
Deref Trait 的定义
rust
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
实现 Deref 后,智能指针可以被自动解引用为内部类型。
示例:Box<T> 的解引用
rust
let x = Box::new(5);
println!("{}", *x); // 解引用 Box,得到内部的 5
*x 实际上是 *(x.deref()) 的语法糖,编译器会自动插入 deref() 调用。
隐式解引用
Deref Coercion
当类型 T 实现了 Deref<Target = U> 时:
&T可以自动转换为&U&mut T可以自动转换为&mut U(如果实现了DerefMut)- 转换可以连续进行,直到目标类型匹配
因此下面代码中&Box<String>类型可以转为&str。
rust
fn hello(name: &str) {
println!("Hello, {}", name);
}
let m = Box::new(String::from("Rust"));
hello(&m); // 自动将 &Box<String> 转换为 &String,再转换为 &str
点操作符的自动解引用
Rust 的方法调用语法(点操作符)会自动执行解引用和引用,使得调用方法非常方便。
rust
let s = Box::new(String::from("hello"));
// len 是 String 的方法,不是 Box 的方法
println!("{}", s.len()); // 自动解引用为 &String
编译器会在必要时插入 * 和 &,直到找到匹配的方法。这个过程称为"自动引用和解引用"。
Deref 与 DerefMut
在 Rust 中,可变引用(&mut T)解引用出来的值可以修改,而不可变引用(&T)解引用出来的值不可以修改。这是Rust解引用的一个基本规则,它还有两个细节:
-
当对智能指针使用
*时,实际调用的是deref()方法,返回一个引用,然后编译器自动取该引用的值(实际上*的结果是deref返回的引用的目标)。 -
如果智能指针实现了
DerefMut,并且位于可变上下文中,则会调用deref_mut()。
rust
let mut x = 10;
let r = &x; // 不可变引用
// *r = 20; // ❌ 编译错误:不能对不可变引用赋值
let r_mut = &mut x; // 可变引用
*r_mut = 20; // ✅ 可以修改
println!("{}", x); // 输出 20
总结
Rust的所有权看上去很复杂,但总结下来也只有很简单的四句话:
- 每个值都有且只有一个所有者
- 所有者离开作用域,值会被回收
- 引用(
&T)可以借用值而不获取所有权 - 任意时刻,一个值只能有一个可变引用 或 多个不可变引用