Skip to content

Instantly share code, notes, and snippets.

@dispensable
Last active November 5, 2020 18:35
Show Gist options
  • Save dispensable/6d69a821b835d2d57480b3d6db83fa58 to your computer and use it in GitHub Desktop.
Save dispensable/6d69a821b835d2d57480b3d6db83fa58 to your computer and use it in GitHub Desktop.
Rust所有权系统笔记

作用

使rust无需垃圾回收即可实现内存回收

所有权规则

  1. Rust中每一个值都有一个称之为其所有者的变量;
  2. 值有且只有一个所有者;
  3. 当所有者(变量)离开作用于,这个值将被丢弃;

字符串字面量和String

// 字符串字面量|被硬编码到二进制文件中,本质是
// 一个slice类型,指向文件的特定位置。不可变
let s = "string ...";

// string类型|在堆上分配内存,可变
let s = string::from("this is a string")// rust在}时自动调用drop| Resource Acquisition is initialization 

变量和数据的交互方式:

  1. 移动/克隆 实现了copy trait的值在移动的时候会被复制(深拷贝)

未实现copy trait的值在移动的时候会创建一个新的引用且使第一个引用无效化,保证只有该值只有一个所有者(浅拷贝)

let s1 = String::from("this is a test");
let s2 = s1.clone();

// s2从s1复制了一份数据,现在两个变量是各自数据的所有者

// 存储在栈上的数据:拷贝
let s3 = "test";
let n4 = 4;

如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。

任何简单的标量值的组合可以是copy的,例如:

  • 所有整数类型;
  • 布尔类型;
  • 所有浮点数类型;
  • 元祖,当且仅当其包含的类型也都是copy的时候

所有权与函数

将值传递给函数和将值赋值给变量类似。 返回值可以转移作用域(移动/转移所有权)

引用与借用

通过&创建一个引用,而不是获取值的所有权

fn main() {
    let s1 = String::from("hello");
    // 通过&获取s1的引用,使用之后再归还
    let len = calculate_length(&s1);
}

// 接收一个引用,返回一个无符号根据机器架构自定义的整数
fn calculate_length(s: &String) -> usize {
    s.len()
}

引用的内存结构

当引用离开作用域后并不会丢弃指向它的数据,因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有所有权。

获取引用作为函数参数称为借用(borrowing)

内存在正确的位置被释放,且只释放一次。

引用与可变性

默认获取的引用是不可变的,如果需要可变的引用,需要声明为可变引用

let s1 = String::from("this is a test");
let s2 = &s1  // 不可变引用,任意时间内可以有多个不可变引用

let s3 = string::from("Hello");
let s4 = &mut s3;  // 获取可变引用
s4.push_str(", world!");
  1. 在任意给定的时间内,只能拥有其中的一个:
    • 一个可变引用
    • 任意数量的不可变引用
  2. 引用必须总是有效的(不能出现悬垂指针);

slice

  • 字符串 slice
let a = String::from("hello world!");

let b = &a[1..2]; // e
let c = &a[..2];
let d = &a[1..];
let e = &a[..];

fn first_word(s: &str) -> &str {
    // some code 
    // 这样的函数签名可以对String也有效,因为String可以通过&string[..]的方式转为slice传入,就无需写 (s: &String) -> &str {} 了
}
  • 其他类型slice
let my_string = [1, 2, 3, 4];
let num23 = &my_string[1..3];  // &[i32]类型

// 可以对所有其他类型的集合使用slice语法

生命周期

生命周期的主要目的是避免悬垂引用| 未经初始化的变量不得使用

通过借用检查器,检查那些引用的变量,如果生命周期不够长,则编译报错

函数中的泛型生命周期

fn logest(s1: &str, s2: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

以上代码的编译会报错,原因是rust不能推断出返回值的生命周期和参数值的生命周期有什么关系,此时需要手工标注生命周期参数来定义引用间的关系以便借用检查器可以进行分析

生命周期的注解语法

生命周期注解并不改变任何引用的生命周期的长,它所做的是将多个生命周期参数联系起来。

本质还是宁可错杀一万,不可放过一个。例如上的代码,因为不确定到底返哪个参数的引用,就假定 返回值的引用是相同生命周期参数里最短的,其实是有可能保证既不悬垂引用(引用值被提前释放), 也能实现函数功能的情况的,但是这个可能也包含了悬垂引用的可能,生命周期标注可以保证不出现 悬垂引用,因为那样的情况借用检查器根本不会允许(它只允许了最安全的情况,在检查上可谓是非常的 激进了)。

fn logest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// <'a>: 因为生命周期参数是泛型的,所以要在函数名之后声明泛型参数
// &'a str: 生命周期参数标记在&之后,类型之前,中间用空格区分
// 例如: &'a i32, &'a mut i32
// 两个生命周期注解有相同的名称意味着这两个值必须同与这相同的泛型生命周期存在的一样久

标注生命周期注解并不改变任何值的生命周期长短,而只是形成一个契约,任何不满足这个契约的的传入值都将被借用检查器拒绝。当具体引用被传递给函数的时候,'a被替换为具体生命周期是s1和s2重叠的那部分作用域。此时我们才知道,返回值的类型引用必须在重叠作用域结束之前有效,所有在重叠作用域之后对该引用的使用都是非法的。

我的理解,生命周期注解的作用就是为了明确函数调用之后,引用的生命周期可以提供怎样的最低保证,从而保证借用检查器的检查工作能够继续而不是因为函数导致的生命周期变化而被打断。

生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。一旦他们形成了某种联系,Rust就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

定义存放引用的结构体也需要生命周期注解:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

生命周期省略

  1. 每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:fn foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。

  2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32。

  3. 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 &self&mut self,那么 self 的生命周期被赋给所有输出生命周期参数。这使得方法编写起来更简洁。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment