浅谈rust所有权机制

122 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

所有权是Rust独有的内存管理机制,这让coder们无需考虑释放内存,同时也获得了比GC语言更多的性能

所有权概述

  • 一个值无论何时只能存在一个拥有者
  • 当值超出作用域时将被清除

所有权作用域

let s = String::from("Rust");
println!("{s}");

上面这段代码会成功运行并输出一个Rust,但如果将代码改成如下呢?

{
    let s = String::from("Rust"); //s拥有着"Rust"
}//作用域结束,s失去所有权
println!("{s}");

运行将会得到结果 not found in this scope,所以值超出作用域时将会被清除,这个作用域包括:

  • 块级作用域
  • 函数作用域
  • 使用了move的闭包

所有权的转移

let s = String::from("Rust");
let s2 = s;
println!("{s}");

上面这段代码将会得到一个value borrowed here after move 这意味着当我们打印s的时候,s的所有权已经被移动给了s2,这就对应着所有权机制的第一个规则\

一个值无论何时只能存在一个拥有者

编写一个函数ownership_transfer_with_tuple

fn main(){
    let s1 = String::from("RUST");
    let (len, s2) = ownership_transfer_with_tuple(s1);
    println!("{s1}");
}

fn ownership_transfer_with_tuple(s: String) -> (usize, String) {
    (s.len(), s)
}

运行同样将会得到一个结果value borrowed here after move,在这里将s1传入了函数中,所有权也一起进入,然后连同长度组成一个元组被分配给s2,所有权就转移到了s2身上,s1此刻就空空如也

所以所有权随着赋值而转移

let t:i32 = 1;
let a  = t;
println!("{t}");

这段代码是可以运行的,但这好像与我们刚讲的相矛盾,为什么呢?

所有权转移原理

众所周知,代码运行时,能接触到的内存空间就是堆和栈(堆栈),基本类型和局部变量等一般都存放在栈上,而一般的对象等需要手动开辟内存空间的数据就存放在堆上

而在堆上创建数据返回的是内存地址头部指针,需要遵循指针来跳跃,比如我新建一个String对象,它在内存空间中的结构类似于这样

image.png

若执行let s2 = s1

image.png 这样s1就失去了所有权,为什么要如此设计?

之前我们提到过,值超出作用域时将会被清除,若所有权没有被移交,当作用域结束时,rust就会尝试释放两个指针,但能释放成功的只有一个,这称为双重释放错误,所以为了保证内存安全,rust不得不这样做

那为什么上面代码的i32类型的数据就没用转移所有权呢?这是因为基本类型的数据都存储在栈上,不同于堆上数据的大小未知这是在编译时期就已知大小的,同时访问速度要比通过指针访问堆上的数据快很多,rust可以快速的复制它从而赋值给一个新的变量,这些都是一个基于一个名叫Copytrait实现的(可以理解为javainterface),实现了这个trait的数据类型在被赋值给其他变量时可以复制自身的值,有以下类型

  • 整数类型,i32.u32等等
  • 字符类型,char
  • 浮点类型,例如f64.
  • 布尔类型 ,true,false
  • 只包含实现了Copy的数据类型的元组,如(i32,f64)