rust学习 -- 第七章 rust中的所有权

78 阅读11分钟

第七章 rust中的所有权

什么是所有权?

  • 在其他的语言中,都需要在运行时对计算机的内存进行管理.
  • 有些语言有垃圾回收机制来处理不再使用的内存.
  • 有些语言,需要显式的分配和释放内存.

rust采用另一种方式来管理内存:所有权.内存管理的工作是在编译时完成的.

stack vs heap

  • stack 先进后出
  • heap 先进先出

所有存储在Stack上的数据必须拥有已知的固定的大小.编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在heap上.

当把数据放入heap时, 会需要请求一定数量的空间.这是操作系统控制的,在heap上找到一个足够大的空间,把他标记为在用, 然后返回一个指针, 就是空间的地址.

当代码调用函数时, 值被传入到函数(也包括指向heap的指针),函数本地的变量被压到stack上, 当函数结束后, 这些值会从stack上弹出.

所有权解决的问题:

  • 跟踪代码的哪些部分正在使用heap的哪些数据
  • 最小化heap上的重复数据量
  • 清理heap上未使用的数据以避免空间不足

一旦你懂得所有权, 那么就不需要经常去想stack或heap了. 但是知道管理heap数据是所有权存在的原因, 这有助于解释它为什么会这样工作.

所有权规则

  • 每个值都有一个变量,这个变量是该值的所有者, 并由他管理
  • 每个值同时只能有一个所有者
  • 当所有者超出作用域(scope)时,该值将被删除.

作用域

fn main(){
    // s is not available
    let s = 6; // s is available
    // you can start operating the s
}// overflow the scope of s, s is disabled

以字符串为例子,讲解所有权的概念

字符串类型有两种形式:

  1. 字符串字面值: 程序里手写的那些字符串值. 他们是不可变的.这些值是直接写在程序代码里的, 存储在栈内存上.
  2. String: 在heap上分配的存储空间, 能够存储在编译时未知数量的文本.

创建string类型的值

可以使用from函数从字符串字面值创建出String类型.let s = String::from("hello");

fn main(){
    let mut s = String::from("hello");
    s.push_str(",World");  // modify string
    println!("{}", s);
} // 超出作用域范围,会将s的内存释放

对于某个值来说, 当拥有它的变量超出他的作用域范围时, 内存会立即自动的交还给操作系统.

从上面所有权的规则和变量的作用域两个方面来讲, 当我们将一个变量赋值给另一个变量时, 数据的所有权就发生了变更, 而当这两个变量超出了作用域的范围后, 都应当被销毁.而此时就存在问题了, 因为两个变量的实际存储的是相同的值, 会不会将相同的内存释放两次. 看如下的例子

fn main(){
    let x = 5;
    let y = x;  // 按照规则,5的所有权交给了y
    println!("{} {}", x, y); // 如果按照之前的逻辑,x,y都需要释放.
}

针对变量和数据(如5)之间的交互,rust设计了一个移动(move) 的概念.

在这里是将数据的所有权交给了y,可以认为是一种浅拷贝, 只是拷贝了数据的引用. 不过如5或者字符串字面值这种简单直接的数据类型, 浅拷贝和深拷贝的区别不大, 因为这些数据本身就在数据栈中, 只是重新加入了一遍.

如果是复杂的数据类型, 比如String类型和数组类型等, 他们的数据是放在堆内存中的, 栈中只存放了数据的引用. 他们的所有权移动(move)是另一种形式了.

以字符串String类型举例:

let s1 = String::from("hello");
let s2 = s1;

一个 String 类型由3部分组成

namevalue
ptr(指向存放字符串内容的内存指针)指向堆内存
拷贝len (一个长度,存放字符串内容所需的字节数)5
capacity(一个容量,string从操作系统总共获得内存的总字节数)5

上面这一部分存放于栈内存中.

堆内存拷贝

indexvalue
0h
1e
2l
3l
4o

在rust中当s2 = s1时,并没有复制堆内存中的东西, 而只是复制了引用,同时rust使s1的引用失效.所以在超出作用域释放内存时,就不用再释放s1了.

同时如果在之后的代码中使用失效的s1变量,是会导致错误的.

fn main(){
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{} {}", s1, s2); // Error: borrow of moved value: 's1'
}

这里隐含的一个设计原则是:rust不会自动创建数据的深拷贝, 都是默认建立的浅拷贝.

当我们需要深拷贝的时候,需要调用克隆(Clone)这个方法

fn main(){
    let x = 5;
    let y = x.clone(); // 深拷贝
    println!("{} {}", x, y);
}

于此对应, 在栈上的数据的拷贝叫 复制.并有一个叫Copy trait的方法.

  • Copy trait, 可以用于像整数这样完全存放在stack上面的类型
  • 如果一个类型实现了Copy这个trait,那么旧的变量在赋值后仍然可用
  • 如果一个类型或者该类型的一部分实现了Drop trait,那么rust不允许他再实现Copy trait

一些拥有Copy trait 的类型

  • 任何简单标量的组合类型都可以是copy的

  • 任何需要分配内存或某种资源的都不是copy的

  • 一些拥有copy trait的类型

    • 所有的整数类型
    • bool
    • char
    • 所有浮点类型
    • tuple类型,如果tuple中所有的字段都是可以copy的,否则不行.

所有权和函数

将值传递给函数和将值传递给变量,产生的效果是相似的.传递给函数只会发生两种情况:移动或者复制.

fn main(){ 
    let s = String::from("Hello World");
    take_ownership(s); // 将s的所有权转向函数内部了, 在外部就不能访问了
    let x = 5;
    makes_copy(x);  // 由于类型的原因,x在外面还是可以访问的
    println!("x: {}", x);
}
​
fn take_ownership(some_string: String)
{
    println!("{}", some_string)
}
​
fn makes_copy(some_number: i32)
{
    println!("{}", some_number);
}

返回值和作用域和所有权

函数在返回值的过程中,同样也会发生所有权的转移

一个变量的所有权总是遵循同样的模式:

  • 把一个值赋给其他变量时就会发生移动
  • 当一个包含heap数据的变量离开作用域时,它的值就会被drop函数清理,除非数据的所有权转移到另一个变量上.
fn main(){
    let s1 = gives_ownership();
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2);
    // 这里s2不再可用
}// s1 s2 s3 都被销毁fn gives_ownership()->String{
    let some_string = String::from("hello");
    some_string  // some_string 的所有权返回到main函数了
}
​
fn takes_and_gives_back(a_string: String)->String{
    a_string  // s2 的所有权传递进来,然后又被传递出去.
}

如何让函数使用某个值,但不获得其所有权?

使用 引用或者借用.什么是引用,将参数的类型变为 &String 而不是String

&符号就表示引用:允许你引用某些值而不取得其所有权

fn main(){
    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() // 超出作用域 但并不清理存储空间,因为并未获得所有权
}

是否能够修改借用? 不可以, 和变量一样, 默认都是不可以修改的.除非我们将他设置为可变的mut.

fn main(){
    let mut s1 = String::from("Hello");
    let len = calculate_length(&mut s1);
​
    println!("the length of '{}' is {}", s1, len);
}
​
fn calculate_length(s: &mut String)->usize{
    s.push_str(", world");
    s.len() // 超出作用域 但并不清理存储空间,因为并未获得所有权
}

可变引用的重要限制是:在特定的作用域内,对某一块数据,只能有一个可变的引用.这样做的好处是可在编译时防止数据竞争.

以下三种行为下会发生数据竞争:

  • 两个或多个指针同时访问同一个数据.
  • 至少有一个指针用于写入数据
  • 没有使用任何机制来同步对数据的访问

可以通过创建新的作用域,来允许非同时的创建多个可变引用

fn main(){
    let mut s = String::from("Hello");
    {
        let s1 = &mut s;
    }
    let s2 = &mut s; // 虽然有两个可变引用,但是他们的作用域不重叠
}

另一个限制:不可以同时拥有一个可变和一个不可变的引用(同一个变量).多个不可变引用是可以的.

引用有引发悬空指针的问题

fn main(){
    let r = dangle();
}
​
fn dangle() -> &String{
    let s = String::from("hello");
    &s  // 返回引用,但是s在函数结束的时候就要被销毁了,这个引用也会失效.  这里会引入一个生命周期的概念,之后会讲
}

字符串切片

字符串切片是指字符串中一部分内容的引用.

形式: [开始索引..结束索引]

  • 开始索引就是切片起始位置的索引值
  • 结束索引是切片终止位置的下一个索引值
fn main(){
    let s = String::from("Hello world");
​
    let hello = &s[0..5];
    let world = &s[6..11];
    println!("{}, {}", hello, world);
}

字符串字面值就是一个切片,字符串字面值被直接存储在二进制程序中.

fn main(){
    let s = "Hello world";  // s是&str 字符串切片类型的
    println!("{}", s);
}

将字符串切片作为参数传递

fn first_word(s: &String) -> &str{}
// 改为
fn first_word(s: &str) -> &str{}

使用方法 :1. 使用字符串切片直接调用该函数.2. 使用String,可以创建一个完整的String切片来调用该函数.

定义函数时使用字符串切片来代替字符串会使我们的api更加通用,且不会损失任何功能.

fn main() {
    let my_string = String::from("Hello world");
    let wordIndex = first_world(&my_string[..]);  // 这里的..
​
    let my_string_literal = "hello_world";  // 本身就是字符串切片类型
    let wordIndex = first_world(my_string_literal);
}
​
fn first_world(s: &str) -> &str{ // 采用字符串切片
    let bytes = s.as_bytes();
    for(i, &item) in bytes.iter().enumerate()
    {
        if item == b' '
        {
            return &s[..];
        }
    }
    &s[..]
}

析构函数

析构函数不仅可以用于管理内存资源, 还能用于管理更多的其他资源, 如文件、 锁、socket等.

rust中编写"析构函数"的办法是 impl std::ops::Drop. Drop trait 定义如下, 在对象消亡时, 自动调用

trait Drop {
    fn drop(&mut self);
}
use std::ops::Drop;
struct D(i32);
impl Drop for D {
    fn drop(&mut self) {
        println!("destruct {}", self.0);
    }
}
fn main() {
    let _x = D(1);
    println!("construct 1");
    {
        let _y = D(2);
        println!("construct 2");
        println!("exit inner scope");
    }  // 退出时调用_y析构方法
    println!("exit main function");
} // 退出时调用_x析构方法

资源管理

在创建变量的时候获取某种资源, 在变量生命周期结束的时候释放资源, 是一种常见的设计模式. 这里的资源, 不仅可以包括内存, 还可以包括其他向操作系统申请的资源, 比如 file. rust会主动释放资源,不需要手动释放

use std::fs::File;
use std::io::Read;
fn main() {
    let f = File::open("/opt/hello/some");  // 文件需要存在
    if f.is_err() {
        println!("file is not exist.");
        return;
    }
    let mut f = f.unwrap();
    let mut content = String::new();
    let result = f.read_to_string(&mut content);  // 返回的是读入的字节数
    if result.is_err() {
        println!("read file error");
        return;
    }
    println!("{}", result.unwrap());
}  // 主动释放资源

主动析构

用户是不能主动调用析构函数的, 这是非法的.

let p = Box::new(100);
p.drop(); // 会报错

那如何才能主动释放能? 使用std::mem::drop 函数

use std::mem::drop;
fn main() {
    let mut v =vec![1,2,3];
    drop(v); // 删除掉变量
    v.push(4);
}

std::mem::drop的原理很简单, 就是将变量move进来, 然后在结束函数, 因为所有权转移进来,但是没有转移出去,变量就被自动释放了.

#[inline]
pub fn drop<T>(_x: T) {}

所以对于实现了 Copy trait 的变量来说, std::mem::drop是没有意义的, 因为传进去的就是一个copy的值, 释放的也是这个值.

变量遮蔽(Shadowing) 不会导致变量生命周期提前结束,它不等同于drop.

如果用下划线来绑定一个变量, 那么这个变量会当场执行析构, 而不是等到当前语句块结束的时候再执行

use std::ops::Drop;
struct D(i32);
impl Drop for D {
    fn drop(&mut self) {
        println!("destructor for {}", self.0);
    }
}
fn main() {
    let _x = D(1);
    let _ = D(2);  // 立即释放
    let _y = D(3);
}