从零学Rust - 所有权与借用

105 阅读7分钟

学习 Rust 的第 4 篇笔记:所有权与借用。所有权是 Rust 最为与众不同的特性,也是难点之一。它的作用是解决内存安全问题,在其它语言中也存在类似的机制,比如:

  • 垃圾回收机制(GC) ,在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
  • 通过函数调用的方式来申请和释放内存,典型代表:C++

所有权机制优势:只在编译时根据一系列规则进行检查,在程序运行期不会有任何性能上的损失。

栈(Stack)与堆(Heap)

对于 Rust,需要能够判断值是位于栈上还是堆上, 因为这会影响程序的行为和性能。

栈和堆的目的:为程序在运行时提供可供使用的内存空间

  • 按照顺序存储值并以相反顺序取出值,即:后进先出
  • 增加数据叫做进栈,移出数据则叫做出栈
  • 数据占用的内存空间:已知且固定大小

  • 堆是一种缺乏组织的数据结构,其中的数据大小未知且可能发生变化

  • 在堆上分配(allocating)内存的过程:

    1. 在堆的某处找到一块足够大的空位
    2. 把该区域标记为已使用,返回一个表示该区域的指针
    3. 将该指针推入栈中,后续通过栈中的指针找到实际内存位置

对比

  • 写入速度:

    • 入栈(快):无需分配新的空间,只需要将新数据放入栈顶
    • 在堆上分配内存(慢):过程更复杂
  • 读取速度:

    • 栈(快):可以直接存储在 CPU 高速缓存
    • 堆(慢):只能存储在内存,且须要访问栈上的指针)
  • 结论:处理器处理分配在栈上数据会比在堆上的数据更加高效

所有权与堆栈

调用函数:参数(堆上指针、局部变量)按顺序入栈,调用结束按相反顺序移除。

所有权:追踪堆上的数据的 分配释放

所有权原则

规则:

  • 每一个值都被一个变量所拥有,该变量被称为值的所有者
  • 一个值只能拥有一个所有者
  • 当所有者离开作用域范围时,这个值将被丢弃(drop)

字符串字面量与 String 类型

字符串字面量:let s ="hello",类型为 &str(字符串Slice)

不适用所有使用字符串的场景,因为:

  • 字符串字面量被硬编码进程序里,是不可变的
  • 字符串的值不一定能在编写代码时得知,比如用户输入

rust 提供了 String 类型:let s = String::from("hello");

  • 根据字面量创建 String 类型变量,::是调用操作符
  • 分配在堆上,存储在编译时大小未知的文本
let mut s = String::from("hello"); // 可变
s.push_str(", world!"); // push_str() 在字符串后追加字面值
println!("{}", s); // 将打印 `hello, world!`

转移所有权 move

// 1. 通过自动拷贝的方式来赋值
let x = 5; // 固定大小的简单值,存在栈中,无需在堆上分配内存。
let y = x; // 自动拷贝,直接复制内存// 2. 字面量,不可变引用
let x: &str = "hello, world"; // 只引用了存储在二进制中的字符串,并没有持有所有权
let y = x; // 仅仅是对该引用进行了拷贝
println!("{},{}",x,y);
​
// 3. String 类型由3方面构成:
// 存储在栈中的堆指针、字符串长度(已使用大小)、字符串容量(堆内存分配的大小)
let s1 = String::from("hello");
let s2 = s1; // 所有权从 s1 转移给 s2,s1 立即失效
println!("{}, world!", s1); // 报错:use of moved value: `s1`
// 假如不 drop s1,离开作用域释放相同的内存,即造成二次释放(double free) 的错误

深拷贝 clone

Rust 永远也不会自动创建数据的 “深拷贝”,任何自动的复制都不是深拷贝

let s1 = String::from("hello");
let s2 = s1.clone(); // 使用 clone 方法深拷贝复制堆上数据
// 对于执行较为频繁的代码(热点路径),使用 clone 会极大的降低程序性能,小心使用
println!("s1 = {}, s2 = {}", s1, s2);

浅拷贝 copy

只发生在栈上。在编译时是已知大小,被存储在栈上的值拥有 copy 特征,即一个旧的变量在被赋值给其他变量后仍然可用。这样的类型包括:

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组(包含的类型必须能 Copy):(i32, i32) 可以,但 (i32, String) 不行
  • 不可变引用 &T ,例如&str,但是注意: 可变引用 &mut T 是不可以 Copy的

函数传值与返回

传值

堆上数据传给函数,所有权从当前作用域转移到函数里

fn main() {
    let s = String::from("hello");  // s 进入作用域
    takes_ownership(s);             // s 的值移动到函数里,所以到这里不再有效
​
    let x = 5;                      // x 进入作用域
    // x 应该移动函数里,但 i32 是 Copy 的,所以在后面可继续使用 x
    makes_copy(x);                  
} // 这里, x 先移出了作用域。然后是 s。但因为 s 的值已被移走,所以不会有特殊操作fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

返回

可以通过函数返回值转移所有权

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值 移给 s1
    let s2 = String::from("hello");     // s2 进入作用域
    // s2 被移动到takes_and_gives_back 中,它也将返回值移给 s3
    let s3 = takes_and_gives_back(s2);  
  
    // s3 移出作用域并被丢弃
    // s2 也移出作用域,但已被移走,所以什么也不会发生
    // s1 移出作用域并被丢弃          
} 
  
// gives_ownership 将返回值移动给调用它的函数
fn gives_ownership() -> String {            
    let some_string = String::from("hello"); // some_string 进入作用域.
    some_string // 返回 some_string 并移出给调用的函数
}
​
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
    a_string  // 返回 a_string 并移出给调用的函数
}
  • 避免了内存的不安全性
  • 新麻烦:总是把一个值传来传去来使用它,语言表达变得非常啰嗦

引用与借用

&T 引用:允许你使用值但不获取其所有权,创建引用的过程称为借用(borrowing)

*T 解引用:解出引用指向的值

let x = 5;
let y = &x;
​
assert_eq!(5, x);
assert_eq!(5, *y);
assert_eq!(5, y); // error[E0277]: can't compare `{integer}` with `&{integer}`

不可变引用

变量默认不可变一样,引用指向的值默认也是不可变的

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
  
    change(&s1); 
}
​
fn calculate_length(s: &String) -> usize {
    s.len() // 允许你使用值,但是不获取所有权
}
​
// cannot borrow `*some_string` as mutable, as it is behind a `&` reference
fn change(some_string: &String) {
    some_string.push_str(", world");
}

可变引用

&mut T:创建可变引用

特点:

  • 同一作用域,特定数据只能有一个可变引用,可以拥有多个不可变引用
  • 可变引用与不可变引用不能同时存在,无法同时借用可变和不可变
  • 编译器会找到某个引用在作用域结束前就不再使用的位置:Non-Lexical Lifetimes(NLL)
  • 禁止悬垂引用(Dangling References),也叫做悬垂指针:值被释放后,指针依然存在
fn main() {
    let mut s = String::from("hello");
    change(&mut s);
  
    let r1 = &mut s;
    // 1. 同一作用域,特定数据只能有一个可变引用:避免数据竞争,竞态条件
    // 多个的指针同时访问同一数据,其中一个指针被用来写入数据,没有同步数据访问的机制
    let r2 = &mut s; // cannot borrow `s` as mutable more than once at a time
    println!("{}, {}", r1, r2);
  
    // 可以通过括号创建一个新的作用域,避免同时拥有
    {
        let r1 = &mut s;
    }
    let r2 = &mut s; // ok
}
​
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
​
// 2. 无法同时借用可变和不可变
// 避免可变引用修改了值,不可变引用没同步
fn exist() {
    let mut s = String::from("hello");
    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    // cannot borrow `s` as mutable because it is also borrowed as immutable
    let r3 = &mut s; 
    println!("{}, {}, and {}", r1, r2, r3);
}
​
// 3. nll,Rust 1.31以后
fn nll() {
   let mut s = String::from("hello");
​
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新编译器中,r1,r2作用域在这里结束
​
    let r3 = &mut s;
    println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
  // 新编译器中,r3作用域在这里结束// 4. 垂悬指针
fn dangle() -> &String { // error[E0106]: missing lifetime specifier
    let s = String::from("hello");
    &s
} // s 离开作用域并被丢弃,其内存被释放,但是函数调用者依然持有指针
// 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