一文了解 Rust 所有权

Rust 是一种系统编程语言,它强调内存安全、并发性和高性能。所有权(Ownership)是 Rust 语言的一个重要特性,它用于管理内存和资源的分配和释放。

堆空间和栈空间

Rust语言区分堆空间和栈空间,虽然它们都是内存中的空间,但使用堆和栈的方式不一样,这也使得使用堆和栈的效率有所区别。

栈空间

栈空间属于操作系统的概念,操作系统负责管理栈空间,负责创建、释放栈帧。

栈空间采用后进先出的方式存放数据(就像叠盘子)。每次调用函数,都会在栈的顶端创建一个栈帧(stack frame),用来保存该函数的上下文数据。比如该函数内部声明的局部变量通常会保存在栈帧中。当该函数返回时,函数返回值也保留在该栈帧中。当函数调用者从栈帧中取得该函数返回值后,该栈帧被释放(实际上不会真的释放栈帧的空间,无效的栈帧可以被复用)。

堆内存

不同于栈空间由操作系统跟踪管理,堆内存是一片无人管理的自由内存区,需要时要手动申请,不需要时要手动释放,如果不释放已经无用的堆内存,将导致内存泄漏,内存泄漏过多(比如在某个循环内不断泄漏),可能会耗尽内存。

Rust 不需要手动释放资源,同时也不支持GC,但是 Rust 编译器会在变量范围结束的时候,自动添加调用释放资源函数的步骤。

Rust 复制代码
{                    
  let s = "hello";   
  
  // 在作用域结束是编译器会自动添加drop函数的调用
  drop(s);
}  

这种机制看似很简单了:它不过是帮助程序员在适当的地方添加了一个释放资源的函数调用而已。但这种简单的机制可以有效地解决一个史上最令程序员头疼的编程问题。

堆和栈****一些特性

  1. 栈适合存放存活时间短的数据

  2. 数据要存放于栈中,要求数据所属数据类型的大小是已知的

  3. 使用栈的效率要高于使用堆

  4. Rust将哪些数据存放于栈中

    1. 裸指针(一个机器字长)、普通引用(一个机器字长)、胖指针(除了指针外还包含其他元数据信息,智能指针也是一种带有额外功能的胖指针,而胖指针实际上又是Struct结构)

    2. 布尔值

    3. char

    4. 各种整数、浮点数

    5. 数组(Rust数组的元素数据类型和数组长度都是固定不变的)

    6. 元组

  5. Rust除了使用堆栈,还使用全局内存区(静态变量区和字面量区)

字符串字面量、static定义的静态变量(相当于全局变量)都会硬编码嵌入到二进制程序的全局内存区。

Rust 复制代码
fn main(){
  let _s = "hello"; 
  let _ss = String::from("hello");
}

上面代码中的几个变量都使用了字符串字面量,且使用的都是相同的字面量"hello",在编译期间,它们会共用同一个"hello",该"hello"会硬编码到二进制程序文件中。

  1. Rust中允许使用const定义常量。常量将在编译期间直接以硬编码的方式内联(inline)插入到使用常量的地方

堆栈的示例

Rust 复制代码
fn main() {
    let n = 33;
    
    let v = vec![1, 2, 3, 4];
}
Rust 复制代码
fn main() {
    let n = 33;
    let nn = &n;
    
    let vv = vec![1, 2, 3, 4];
    let v = &vv;
}

所有权

作用域

在Rust中,任何一个可用来包含代码的大括号都是一个单独的作用域**,**包括且不限于以下几种结构中的大括号都有自己的作用域:

  • if、while等流程控制语句中的大括号

  • match模式匹配的大括号

  • 单独的大括号

  • 函数定义的大括号

  • mod定义模块的大括号

Rust 复制代码
#![allow(unused)]
fn main() {
    {                    // s 在这里无效, 它尚未声明
      let s = "hello";   // 从此处起,s是有效的
      println!("{}", s); // 使用 s
    }                    // 此作用域已结束,s不再有效
}

函数作用域内,无法访问函数外部的变量,而其他大括号的作用域,可以访问大括号外部的变量

Rust 复制代码
fn main() {
  let x = 32;
  fn f(){
    // 编译错误,不能访问函数外面的变量x和y
    // println!("{}, {}", x, y);  
  }
  let y = 33;
  f();

  let mut a = 33;
  {
    // 可以访问大括号外面的变量a
    a += 1;
  }
  println!("{}", a);
}

变量遮盖:在可以访问大括号外部的变量的作用域内定义的变量可以遮盖外部变量

Rust 复制代码
fn main() {
  let x = 32;
  fn f(){
    // 编译错误,不能访问函数外面的变量x和y
    // println!("{}, {}", x, y);  
  }
  let y = 33;
  f();

  let mut a = 33;
  {
    // 可以访问大括号外面的变量a
    a += 1;
  }
  println!("{}", a);
}

所有权规则

Rust所有权规则可以总结为如下几句话:

  • Rust中的每个值都有一个被称为其所有者的变量(即:值的所有者是某个变量)

  • 值在任一时刻有且只有一个所有者

  • 当所有者(变量)离开作用域,这个值将被销毁

这里对第三点做一些补充性的解释,所有者离开作用域会导致值被销毁,这个过程实际上是调用一个名为drop的函数来销毁数据释放内存。在前文解释作用域规则时曾提到过,销毁的数据特指堆栈中的数据,如果变量绑定的值是全局内存区内的数据,则数据不会被销毁。

例如:

Rust 复制代码
fn main(){
  {
    let mut s = String::from("hello");
  } // 跳出作用域,栈中的变量s将被销毁,其指向的堆中数据也被销毁,但全局内存区的字符串字面量仍被保留
  
  print!("{}",s)
}

数据交互

在其他语言中,有深拷贝和浅拷贝的概念,浅拷贝描述的是只拷贝数据对象的引用,深拷贝描述的是根据引用递归到最终的数据并拷贝数据。

在Rust中没有深浅拷贝的概念,但有移动(move)、拷贝(copy)和克隆(clone)的概念。

拷贝(copy)

Rust 复制代码
let x = 5;
let y = x;

这个程序将值 5 绑定到变量 x,然后将 x 的值复制并赋值给变量 y。现在栈中将有两个值 5。此情况中的数据是"基本数据"类型的数据,不需要存储到堆中,仅在栈中的数据的"移动"方式是直接复制,这不会花费更长的时间或更多的存储空间。"基本数据"类型有这些:

  • 所有整数类型,例如 i32 、 u32 、 i64 等。

  • 布尔类型 bool,值为 true 或 false 。

  • 所有浮点类型,f32 和 f64。

  • 字符类型 char。

  • 仅包含以上类型数据的元组(Tuples)。

移动(move)

Rust 复制代码
let s1 = String::from("hello");
let s2 = s1;

两个 String 对象在栈中,每个 String 对象都有一个指针指向堆中的 "hello" 字符串。在给 s2 赋值时,只有栈中的数据被复制了,堆中的字符串依然还是原来的字符串。

当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存。但是 s1 和 s2 都被释放的话堆区中的 "hello" 被释放两次,这是不被系统允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了。没错,在把 s1 的值赋给 s2 以后 s1 将不可以再被使用。下面这段程序是错的:

Rust 复制代码
let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // 错误!s1 已经失效

所以实际情况是:s1 名存实亡。

克隆(clone)

虽然实现Copy Trait可以让原变量继续拥有自己的值,但在某些需求下,不便甚至不能去实现Copy。这时如果想要继续使用原变量,可以使用clone()方法手动拷贝变量的数据,同时不会让原始变量变回未初始化状态。

Rust 复制代码
fn main(){
  let s1 = String::from("hello");
  // 克隆s1,克隆之后,变量s1仍然绑定原始数据
  let s2 = s1.clone();
  println!("{},{}", s1, s2);
}

不是所有数据类型都可以进行克隆,只有那些实现了Clone Trait的类型才可以进行克隆,常见的数据类型都已经实现了Clone,因此它们可以直接使用clone()来克隆。

要注意Copy和Clone时的区别,如果不考虑自己实现Copy trait和Clone trait,而是使用它们的默认实现,那么:

  • Copy时,只拷贝变量本身的值,如果这个变量指向了其它数据,则不会拷贝其指向的数据

  • Clone时,拷贝变量本身的值,如果这个变量指向了其它数据,则也会拷贝其指向的数据

也就是说,Copy是浅拷贝,Clone是深拷贝,Rust会对每个字段每个元素递归调用clone(),直到最底部。

例如:

Rust 复制代码
fn main() {
  let vb0 = vec!["s1".to_string()];
  let v = vec![vb0];
  println!("{:p}", &v[0][0]);     // 0x21c43a20c50

  let vc = v.clone();
  println!("{:p}", &vc[0][0]);  // 0x21c43a20b70
}

涉及函数的所有权机制

函数参数所有权机制

函数参数类似于变量赋值,在调用函数时,会将所有权移动给函数参数。

Rust 复制代码
fn main() {
    let s = String::from("hello");
    // s 被声明有效

    takes_ownership(s);
    // s 的值被当作参数传入函数
    // 所以可以当作 s 已经被移动,从这里开始已经无效

    let x = 5;
    // x 被声明有效

    makes_copy(x);
    // x 的值被当作参数传入函数
    // 但 x 是基本类型,依然有效
    // 在这里依然可以使用 x 却不能使用 s

} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放


fn takes_ownership(some_string: String) { 
    // 一个 String 参数 some_string 传入,有效
    println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放

fn makes_copy(some_integer: i32) { 
    // 一个 i32 参数 some_integer 传入,有效
    println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释放

函数返回值所有权机制

函数返回时,返回值的所有权从函数内移动到函数外变量。

Rust 复制代码
fn main() {
    let s1 = gives_ownership();
    // gives_ownership 移动它的返回值到 s1

    let s2 = String::from("hello");
    // s2 被声明有效

    let s3 = takes_and_gives_back(s2);
    // s2 被当作参数移动, s3 获得返回值所有权
} // s3 无效被释放, s2 被移动, s1 无效被释放.

fn gives_ownership() -> String {
    let some_string = String::from("hello");
    // some_string 被声明有效

    return some_string;
    // some_string 被当作返回值移动出函数
}

fn takes_and_gives_back(a_string: String) -> String { 
    // a_string 被声明有效

    a_string  // a_string 被当作返回值移出函数
}

引用与借用

不可变引用

所有权不仅可以转移(原变量会丢失数据的所有权),还可以通过引用的方式来借用数据的所有权,&s表示创建变量s的引用,为某个变量创建引用的过程不会转移该变量所拥有的所有权。

Rust 复制代码
fn main(){
  {
    let s = String::from("hello");
    let sf1 = &s; // 借用
    println!("{}, {}, {}",s, sf1);
  }  // sf1离开,s离开
}

多个不可变引用可共存(可同时读)

Rust 复制代码
fn main(){
  {
    let s = String::from("hello");
    let sf1 = &s; // 借用
    let sf2 = &s; // 再次借用
    println!("{}, {}, {}",s, sf1, sf2);
  }  // sf2离开,sf1离开,s离开
}

(不可变)引用实现了Copy Trait,因此下面的代码是等价的:

Rust 复制代码
// 多次创建s的引用,并将它们赋值给不同变量
let sf1 = &s;
let sf2 = &s;

// 拷贝sf1,使得sf2也引用s,
// 但sf1是引用,是可Copy的,因此sf1仍然有效,即仍然指向数据
let sf1 = &s;
let sf2 = sf1;

还可以将变量的引用传递给函数的参数,从而保证在调用函数时变量不会丢失所有权。

Rust 复制代码
fn main(){
  let s = String::from("hello");
  let s1 = s.clone();

  // s1丢失所有权,s1将回到未初始化状态
  f1(s1); 
  // println!("{}", s1);

  // 传递s的引用,借用s所有权 
  let l = f2(&s);
              // 交还所有权
  // s仍然可用
  println!("{} size: {}", s, l);
}

fn f1(s: String){
  println!("{}", s);
}

fn f2(s: &String)->usize{
  s.len()   // len()返回值类型是usize
}

不可变引用是不能修改被引用的值的

Rust 复制代码
fn main() {
    let s1 = String::from("run");
    let s2 = &s1;
    println!("{}", s2);
    s2.push_str("oob"); // 错误,禁止修改租借的值
    println!("{}", s2);
}

可变引用

如果想修改引用的值,我们可以使用可变引用,如要使用可变引用去修改数据值,要求:

  • var的变量可变,即let mut var = xxx

  • var的引用可变,即let varf = &mut var

Rust 复制代码
fn main() {
    let mut s1 = String::from("run");
    // s1 是可变的

    let s2 = &mut s1;
    // s2 是可变的引用

    s2.push_str("oob");
    println!("{}", s2);
}

同样可变引用也可以传递给函数作为参数

Rust 复制代码
fn main() {
        let mut s1 = String::from("run");
    
    push_str(&mut s1);
        println!("{}", s1);
}

fn push_str(s: &mut String) {
        s.push_str("oob");
}

可变引用具有排他性,某数据在某一时刻只允许有一个可变引用,此时不允许有其他任何引用

下面的代码会报错:cannot borrow x as mutable more than once at a time。

Rust 复制代码
#![allow(unused)]
fn main() {
    let mut x = String::from("junmajinlong");
    let x_mut1 = &mut x;    // (1)
    let x_mut2 = &mut x;    // (2)
    println!("{}", x_mut1); // (3)
    println!("{}", x_mut2); // (4)
}

可变引用的排他性更加复杂,具体可以参考:理解可变引用的排他性 - Rust入门秘籍

容器集合类型的所有权规则

前面所介绍的都是标量类型的所有权规则,此处再简单解释一下容器类型(比如tuple/array/vec/struct/enum等)的所有权。

容器类型中可能包含栈中数据值(特指实现了Copy的类型),也可能包含堆中数据值(特指未实现Copy的类型)。例如:

Rust 复制代码
let tup = (5, String::from("hello"));

容器变量拥有容器中所有元素值的所有权

因此,当上面tup的第二个元素的所有权转移之后,tup将不再拥有它的所有权,这个元素将不可使用,tup自身也不可使用,但仍然可以使用tup的第一个元素。

Rust 复制代码
#![allow(unused)]
fn main() {
let tup = (5, String::from("hello"));

// 5拷贝后赋值给x,tup仍有该元素的所有权
// 字符串所有权转移给y,tup丢失该元素所有权
let (x, y) = tup;    
println!("{},{}", x, y);   // 正确
println!("{}", tup.0);     // 正确
println!("{}", tup.1);  // 错误
println!("{:?}", tup);  // 错误
}

如果想要让原始容器变量继续可用,要么忽略那些没有实现Copy的堆中数据,要么clone()拷贝堆中数据后再borrow,又或者可以引用该元素。

Rust 复制代码
#![allow(unused)]
fn main() {
// 方式一:忽略
let (x, _) = tup;
println!("{}", tup.1);  //  正确

// 方式二:clone
let (x, y) = tup.clone();
println!("{}", tup.1);  //  正确

// 方式三:引用
let (x, ref y) = tup;
println!("{}", tup.1);  //  正确
}

一些例子

  1. 解引用时需要移动,引用没有所有权,所以直接解引用时会报错:
Rust 复制代码
let v = &vec![11, 22];
let vv = *v;

// error[E0507]: cannot move out of `*v` which is behind a shared reference
  1. 解引用时可以使用"解引用"clone、引用"解引用"来实现所有权转移
Rust 复制代码
#![allow(unused)]
fn main() {
let a = &"junmajinlong.com".to_string();
// let b = *a;         // (1).取消注释将报错
let c = (*a).clone();  // (2).正确
let d = &*a;           // (3).正确

let x = &3;
let y = *x;      // (4).正确
}
  1. 被遗弃的Move
Rust 复制代码
fn main(){
  let x = "hello".to_string();
  x;   // 发生Move,相当于 let _tmp = x;
  println!("{}", x);  // 报错:value borrowed here after move
}
相关推荐
brrdg_sefg4 小时前
Rust 在前端基建中的使用
前端·rust·状态模式
m0_748230944 小时前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
SomeB1oody12 小时前
【Rust自学】6.1. 定义枚举
开发语言·后端·rust
SomeB1oody12 小时前
【Rust自学】5.3. struct的方法(Method)
开发语言·后端·rust
itas1091 天前
Rust调用C动态库
c语言·rust·bindgen·bindings·rust c绑定
SomeB1oody1 天前
【Rust自学】5.1. 定义并实例化struct
开发语言·后端·rust
m0_748236111 天前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
SomeB1oody2 天前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
SomeB1oody3 天前
【Rust自学】4.2. 所有权规则、内存与分配
开发语言·后端·rust
SomeB1oody3 天前
【Rust自学】4.5. 切片(Slice)
开发语言·后端·rust