Administrator
发布于 2025-12-02 / 9 阅读
0

RUST学习

前言

这门语言准备作为开的新坑,日常记录学习进度

参考:https://github.com/sunface/rust-course

RUST语言作为目前最受欢迎的语言之一,相较于类似go语言有诸多优点

底层、安全、追求极致性能

可以把Rust当作更安全版的C++,但是语法更现代,性能接近甚至超越C++

拿go举例子,内存管理方式上:

Go语言:有垃圾回收(GC)

  • 也就是在new/make一些东西后,不用管什么时候释放

  • GC会自动在后台回收不用的内存

Rust语言:直接就没有GC,取而代之的是用“所有权(Ownership)”和“生命周期(Lifetime)”管理内存

  • 在编译时就会严查:谁拥有这块内存、什么时候移动、什么时候释放

  • 绝大多数内存错误都会在编译时就报错

据说学习所有权和借用规则需要花费大量时间适应,同时编译极其严格,对初学者不是很友好

但是这些“缺点”换来了rust极高的安全性,以及表现形式上更突出,性能非常高

安装方式是通过官网下载rustup后,会自带Cargo和rust,下载链接在下面

需要知道的是,我这里使用的是Windows电脑在下载rustup之前,需要有C++环境

我这里的解决方式是直接通过Vistual Studio里的C++

https://rustup.rs/

一. Rust语言初学

1. 认识Cargo

简单过一遍什么是Cargo,Cargo就相当于nodejs里的npm包管理工具,防止找不到老版本的依赖,从而无法运行很老的一些项目

通过Cargo构建第一个项目

新建一个文件夹专门放rust项目,我这里用的vscode打开文件夹之后,在控制台输入:

cargo new hello_world
cd hello_world 

这样就使用cargo new创建好了一个新项目,老版本还要在后面加--bin,现在不用输入cargo就会默认创建一个bin类型的项目(PS:Rust项目主要有两个类型:bin可运行项目 和 lib依赖库项目)

运行项目

从上文可以看到,bin项目已经被成功搭建了

接下来就是让他跑起来,这里有两种方式

cargo run
cargo build
./target/debug/hello_world

其实第一条等同于运行了两条指令,先对项目进行了编译,然后再运行

第二段代码实际上就完整呈现了这个过程,先编译项目,再以debug模式运行

这里debug的好处在于编译速度非常快,但是运行起来速度就很慢,因为这个模式下,rust编译器不会做任何优化

如果想要高性能的代码,可以在后面加--release

cargo run --release

cargo build --release
./target/release/hello_world

这样编译器会做大量优化,虽然编译速度会慢一些,但是运行起来很快

Cargo check

随着项目的增大,cargo run和cargo build会不可避免的变慢

但如果我只是想验证代码准确性,看看他能不能跑,总不能一次一次等着它运行吧

这个时候就需要它

cargo check

它是在Rust开发中最常用的命令

它的作用就是快速检查代码能不能通过编译,这个代码速度非常快,能减少非常多的编译时间

Cargo.toml和Cargo.lock文件

这两个文件是Cargo的核心文件,Cargo的活动都是基于这两个文件

Cargo.toml是Cargo特有的项目数据描述文件,他是Rust项目的“说明书”+“配置文件”,存储了项目所有的配置信息,负责让项目按照我们预期的方式进行构建

cargo.lock是cargo根据.toml文件生成的项目依赖清单,一般不用动

package配置段落和定义项目依赖

package中记录了项目的描述信息

  • name定义了项目名称

  • version定义了当前版本,默认就是0.1.0

  • deition定义了使用的rust大版本

使用cargo最大的好处就是,它能够对项目的各种依赖项进行方便、统一、灵活的管理

再cargo.toml中,主要通过各种依赖段落描述项目的各种依赖项,有三种形式:

  • 基于rust官方仓库crates.io,通过版本说明来描述

  • 基于项目源代码的git仓库地址,通过URL来描述

  • 基于本地项目的绝对路径或相对路径,通过类Unix模式的路径来描述

三种形式具体写法如下:

[dependencies]
rand     = "0.3"                                       # 1. crates.io + 版本
hammer   = { version = "0.5.0" }                       # 1. crates.io + 版本(完整版写法)
color    = { git = "https://github.com/bjz/color-rs" } # 2. Git 仓库
geometry = { path = "crates/geometry" }                # 3. 本地路径

2. Rust语言初印象

先看这段代码来对Rust语言有个最基本的印象

fn greet_world() {
    let southern_germany = "Grüß Gott!";
    let chinese = "世界,你好";
    let english = "World, hello";
    let regions = [southern_germany, chinese, english];
    for region in regions.iter() {
        println!("{}", &region);
    }
}

fn main() {
    greet_world();
}

从这部分可以看到几点:

  • Rust原生支持UTF-8编码的字符串,无论是直接写中文还是德文都没有问题

  • println!这部分里的“ ! ”是一个“”操作符,可以先把他理解为“特殊函数”

  • 占位符使用{ },Rust语言会自动识别输出数据的类型,可以不用像其他语言又是%s、%d

  • 在Rust里集合类型不能直接循环,需要变成迭代器(这里用的.iter() 方法)才能用于迭代循环

二. Rust基础入门

2.1 变量绑定与解构

为什么手动设置变量的可变性

在其他多数语言中,要么只支持声明可变的变量,要么只支持声明不可变的变量,前者有更多的灵活性,后者则是由更高的安全性,而Rust既要灵活性又要安全性

因为将变量本身无需改变的变量声明为不可变的,在运行上会避免一些多余的runtime检查

变量命名

命名和其他语言没区别,但是需要遵循Rust命名规范

因为Rust语言有一些关键字,和其他语言一样,这些关键字都是保留给Rust语言使用的,因此,它们不能被用作变量或者函数的名称;具体查看https://course.rs/appendix/keywords.html

变量绑定

在其他语言中,通过var a = "hello world"方式给变量a赋值;而rust中,使用let a = "hello world",但是这里不叫赋值,而是叫变量绑定

之所以是绑定是因为涉及了rust核心原则“所有权”,每个对象都有主人,且一般情况下完全属于它的主人,绑定就是把这个对象绑定给了一个变量,让变量成为它的主人,而该对象之前的主人就会丧失该对象的所有权

变量可变性

rust的变量默认情况下是不可变的,是为了保障性能和安全性;

但是我们可以通过mut关键字让变量变成可变的,增加灵活性

先来一个错误示范

fn main() {
    let a = 1;
    println!("这个a的值是{}", a);
    a = 2;
    println!("这个a的值是{}", a);
}

可以看到,cargo run以后直接就是一个报错

原因是无法对不可变的变量进行重复赋值,错误后面还贴心的给出了help,只需要在变量名前加一个mut就可以

fn main() {
    let mut a = 1;
    println!("这个a的值是{}", a);
    a = 2;
    println!("这个a的值是{}", a);
}

这样就可以让变量声明为可变

通常根据场景来决定变量是可变还是不可变

如果是大型数据结构中被频繁调用,那最好是不可变,保证安全性和性能

如果是小型数据结构,可以考虑用较少的性能换取更高的可读性

使用下划线开头忽略未使用的变量

如果创建了一个变量,但是全程没有使用它,Rust通常会给出警告

这个时候可以通过变量名前加下划线,告诉Rust不要警告这个未使用的变量

变量解构

let表达式不但可以用于变量的绑定,还能进行复杂变量的解构

就是从一个复杂的变量中,匹配出该变量的一部分内容

fn main() {
    let (a, mut b): (bool, bool) = (true, false);
    // a = true,不可变; b = false,可变
    println!("a = {:?}, b = {:?}", a, b);

    b = true;
    assert_eq!(a, b);
}

可以看到,这里a和b先被打印后,呈现a=true,b=false,之后可变变量b又被改成了true,二者相等,所以没有报错

【扩展:assert_eq! 是 Rust 里一个断言宏,意思是:断言“两边相等”,如果不相等,就立刻报错(panic)】

解构赋值

在Rust1.59版本后,可以在赋值语句的左边式子中使用元组、切片和结构体模式了

struct Struct {
    e: i32
}

fn main() {
    let (a, b, c, d, e);

    (a, b) = (1, 2);

    // _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
    [c, .., d, _] = [1, 2, 3, 4, 5];

    Struct { e, .. } = Struct { e: 5 };

    assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
}

分析一下这段代码,一共是三种解构赋值方式

最头上的定义了一个结构体,里面有个e,可以先不管

第一次解构是对a和b,使用的是元组解构赋值,把1和2分别赋值给了a和b

第二次解构是对c和d,使用的是切片解构赋值,首先把1赋值给了c,然后中间的都忽略,把4赋值给了倒数第二个元素d,最后一个我使用” _ "表示我不需要它,直接丢掉,这样就成功给c和d赋值上了1和4

第三次解构是对e,使用的是结构体解构赋值,从结构体里把字段e的值5取出来,赋值给e

变量和常量的差异

变量就像上面说的,可以是可变(mut)也可以是不可变

但是常量不一样,常量绑定到常量名就不可更改了,他不允许用mut,自始至终都不能变

常量使用const关键字声明,且值的类型必须标注

const MAX_POINTS: u32 = 100_000;

上面这个就是定义常量的方式,其常量名为 MAX_POINTS,值设置为 100,000

Rust常量命名约定是全部字母都大写,使用下划线分隔单词另外对数字字面量可插入下划线以提高可读性

变量遮蔽shadowing

Rust允许声明相同的变量名,在后面的变量名会遮蔽掉前面声明的

fn main() {
    let x = 5;
    // 在main函数的作用域内对之前的x进行遮蔽
    let x = x + 1;

    {
        // 在当前的花括号作用域内,对之前的x进行遮蔽
        let x = x * 2;
        println!("The value of x in the inner scope is: {}", x);
    }

    println!("The value of x is: {}", x);
}

首先先将数值5绑定到x,然后重复使用let x =遮蔽之前的x,并取原来的值+1,这时x就变成了6,之后又同样的方式遮蔽了前面的x,让他取原来的值*2,然后x最终又变成了12

可以看到,不同作用域里的x值都是不一样的,虽然都叫x,但是却是不同的变量

mut使用不同,第二个 let 生成了完全不同的新变量,两个变量只是恰好拥有同样的名称涉及一次内存对象的再分配 ,而 mut 声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好。

变量遮蔽作用在于,如果在一个作用域内不需要使用之前的变量,可以重复使用变量名字,不用为命名发愁,但是遮蔽之后无法访问到前面的同名变量

假设有一个程序要统计一个空格字符串的空格数量:

// 字符串类型
let spaces = "   ";
// usize数值类型
let spaces = spaces.len();

分析一下这段代码,首先声明了一个spaces变量,内容是三个空格;之后通过变量遮蔽又定义了一个spaces,右式则是求上面最初定义的spaces这个字符串的长度,三个空格,所以新的spaces = 3

这种结构允许的第一个spaces变量是字符串类型,第二个spaces是一个全新变量,只是名字相同,且是数值类型

let mut spaces = "   ";
spaces = spaces.len();

这种情况是不允许的,因为上面的spaces已经确定类型为字符串,而下面的则是数值类型,不允许将整数类型 usize 赋值给字符串类型

2.2 ⭐所有权和借用

Rust之所以能成为最受喜爱的语言之一,重点就在于它的安全性,在前言里也提到,诸多语言几乎都是通过GC的方式(前言有讲)实现内存安全,但是,GC好用的同时会拖慢性能、内存占用等问题,在要求高性能的场景是不可接受的,因此,Rust采用的是所有权系统

所有权

所有程序都是和内存打交道,如何从内存中申请空间来存放程序的运行内容,以及如何在不需要的时候释放内存空间成了关键问题

随着语言发展分成了三个流派:

  • 垃圾回收机制(GC):前言已经提及过了,他会不停的寻找不再使用的内存,典型代表:java、Go

  • 手动管理内存的分配和释放:在程序中通过函数调用的方式来申请和释放内存,典型代表C++

  • 通过所有权来管理内存:编译器在编译的时候就会根据一系列规则进行检查

为什么说Rust的所有权比较难学但是还要学的原因之一,就是检查只发生在编译期,在程序运行期间,不会损失任何性能,大大提高了性能

int* foo() {
    int a;          // 变量a的作用域开始
    a = 100;
    char *c = "xyz";   // 变量c的作用域开始
    return &a;
}                   // 变量a和c的作用域结束

先来看这段代码,a和c都是局部变量,但在函数结束的时候把局部变量a的地址返回了,a存在于栈中,在离开作用域后,a所申请的栈上内存都会被系统回收,从而造成了悬空指针问题。这段代码可以通过编译,但是会在运行上出现错误

再看变量c,c的值是常量字符串,存储在常量区,可能这个常量在一段代码中只会使用一次,但是“xyz”只有当整个程序结束后系统才会回收这部分内存

栈Stack与堆Heap

栈和堆是编程语言中最核心的数据结构,对于Rust语言非常关注值是位于栈上还是堆上,因为会影响程序的行为和性能

栈按照顺序存储值并且以相反顺序取出值,也就是后进先出,原理就像叠盘子,每增加一个盘子都会叠到盘子堆的顶上,取出盘子时,只能从顶部取,不能从中间也不能从底部取出盘子

增加数据叫做进栈,取出数据叫做出栈

栈中所有的数据必须占据已知且固定的大小的内存空间,如果数据大小是未知的,在取出数据时将无法取到想要的数据

和栈不同的是,栈存储的是数据大小已知且固定的数据,而对于大小未知或可能变化的数据,就需要存储在堆

在把数据存储到堆的过程中,需要申请一定大小的内存空间,然后操作系统在堆的某个位置找到一块足够大的空位,把他标记成已经使用,并返回一个表示这个位置地址的指针,这个过程叫做在堆上分配内存

接下来,上面返回的那个存放地址的指针会被放入栈中,因为它的数据大小已知且固定,后续使用中,将通过栈里的指针来获取数据在堆上的实际位置从而访问这个数据

(上面的过程说简单点,就把内存比喻成一大片地皮,张三想要在这块地皮上安家,就先去找开发商,从这一大块地皮上划出了一块没人住且足够大的范围住了进去,开发商这个时候就会说,这个位置现在有人啦,然后把刚刚划进去的地皮标记成张三的,写在地址簿(栈)上,有人想要找张三,直接从地址簿里找人就好了 ps:把栈比喻成地址簿可能有些不恰当,但是凑合理解)

堆和栈的性能区别

其实从条件上来看就能知道,栈上分配内存比堆上分配内存要快,因为入栈时操作系统不需要进行函数调用(或系统调用-更慢)来分配新的空间,只需要把新数据放到栈的顶部就行了;但是相比之下,堆就需要让操作系统先给他找一块足够存放数据的内存空间,接着做一些记录,为下次分配做准备,如果进程分配的内存不够的时候,还需要再去进行系统调用来申请更多内存;所以处理器在栈上分配数据比在堆上更高效

所有权与堆栈

当代码中调用一个函数的时候,传给函数的参数(包括可能指向堆上数据的指针和函数局部变量)的会被依次压入栈中,当函数调用结束之后,这些值再以入栈时相反的顺序被依次移除

因为在堆上缺乏组织性,因此跟踪这些数据什么时候被分配、什么时候被释放就极其重要,否则堆上的数据将产生内存泄漏,这些数据将永远不能被回收

所有权原则

  1. Rust中每一个值都被一个变量所拥有,这个变量就被称为值的拥有者

  2. 一个值同时只能被一个变量所拥有

  3. 当所有者(变量)离开作用域范围的时候,这个值就会被丢掉(drop)

对于变量作用域和Go语言一样,就不再过多赘述

string类型

这里讲的主要是在rust语言里string和其他语言里的区别

首先是字符串字面量,注意这个字面量,也就是let s = "hello";

它的类型是&str,和其他语言不同的是:他是被硬编码在程序里的(放在只读的常量区),是不可变的

这代表他是写死在程序里,当字符串需要在运行时,通过用户动态输入后存储到内存中的情况,就不能使用它

Rust还另外提供了一种可以增长的、堆上分配的字符串类型String,它和上面那个不同的是:

  1. 通过String::from("hello")创建(::是一种调用操作符,这里表示调用string类型中的from关联函数)

  2. 可以动态变长,比如push_str(", world!")

  3. 因为是存放在堆上,所以它可以存一些运行时才知道的内容,就比如用户输入

let mut s = String::from("hello");

s.push_str(", world!"); // push_str() 在字符串后追加字面值

println!("{}", s); // 将打印 `hello, world!`

2.3 变量绑定背后的数据交互

转移所有权

let x = 5;
let y = x;

这段代码中并没有所有权转移,因为代码先将5绑定到变量x,接着拷贝x的值给y,最后x和y都等于5,因为整数是rust的基本数据类型,固定大小的简单值,所以这两个值都是通过自动拷贝的方式来赋值的,都被存放在栈中,不需要在堆上分配内存

整个过程中的赋值都是通过值拷贝的方式完成(发生在栈中),因此并不需要所有权转移。

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

这段代码则不是上述情况,因为基本类型是存储在栈上,rust可以自动拷贝,但是String不是基本类型,他是在堆上,无法自动拷贝

String类型是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成,最重要的堆指针上文已经阐述过了,容量是堆内存分配的空间大小,长度则是目前已使用的大小

先来看一下String存进去以后是什么状态:

栈上

s1: String {
    ptr: 0x1234,   // 指向堆上数据的指针
    len: 5,        // 当前字符串长度
    capacity: 5    // 目前为它分配的堆空间大小
}

堆上

[ 'h', 'e', 'l', 'l', 'o' ]   // 也就是 "hello" 的字节

String类型指向了一个堆上的空间,里面存放着它的真实数据,let s2 = s1;就要分情况讨论

  1. 拷贝String和存储在堆上的字节数据 如果这条语句是拷贝所有数据(深拷贝),那无论是String本身还是底层的堆上数据,全部会被拷贝,也就是栈上的那些数据以及堆上的hello字节,非常影响性能

  2. 只拷贝String本身 也就是只拷贝栈上的,不拷贝堆上的,运行起来非常快,但是又有一个问题,这样s1和s2都指向的是堆上的“hello”,这样他就有了两个所有者s1和s2,这违背了一个值只允许有一个所有者

假如一个值真的有两个所有者会发生什么?

当变量离开作用域后,Rust会调用drop函数并清理内存,但是有两个String变量都指向同一个位置,s1和s2都会尝试释放相同的内存,这就导致了二次释放的错误,两次释放相同内存会导致内存污染,可能导致潜在安全漏洞

因此,在s1被赋予给s2后,Rust就会认为这个s1不再有效,因此在s1离开作用域后无需丢弃任何东西;也就是说,把所有权从s1转移到了s2,s1 在被赋予 s2后就马上失效了,同时还证明了这个过程并不是浅拷贝,而是s1被移动到了s2

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}.world", s1);
}

这个所有权转移在str上是不会出现的

2.4 函数的传值与返回

将值传递给函数,也会发生移动或复制,和let语句一样

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 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 移出作用域。不会有特殊操作

和上面一样String这种想要传进函数就需要把值移动到函数里,因为函数并没有返回,所以并没有被移动回来,fn main( ){ }里的s之后便不再有效,就比如takes_ownership(s)之后,尝试再加入一个println!("在move进函数后继续使用s: {}",s);;就会报错;而整数型这种简单类型只是把值拷贝进去,后面还是能使用

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                             // 调用它的函数

    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 并移出给调用的函数
}

这里函数有返回值,函数就会把移动给它的值重新移动回去

引用和借用

如果只支持通过转移所有权的方式获取这个变量的值,那么会让程序变得很复杂,这时Rust可以和其他编程语言一样使用变量的指针或者引用

Rust通过借用达成上述目的,获取变量的引用称为借用,再借用完成之后还需要还给他

引用和解引用

常规引用是一个指针类型,指向对象存储的内存地址

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

变量x中存放一个i32值5,y是x的一个引用,上述代码中可以看到,可以直接断言x等于5,但是对于y的值做出断言就必须使用*y解出引用所指向的值(解引用),解引用后的*y就可以访问y所指向的整型值并且进行数值比较——如果是直接写这句assert_eq!(5, y);会编译错误

不可变引用

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()
}

在这段代码中,使用的是s1的引用作为参数传递给下面那个函数,而不是直接把s1的所有权转义给该函数,这样就能不影响s1的后续使用

注意两点:

  1. 不需要和上面章节一样先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁

  2. 可以看到函数里参数s的类型从String变成了&String

“&”符号就是引用,允许使用它的值,但是不获取所有权

通过&s1,创建了一个指向s1的引用,但并不是拥有它。因为并不拥有这个值,当引用离开作用域后,它指向的值也不会被丢弃

能否尝试修改接过来的变量呢?

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

这段编译起来会报错,但不是因为不能改,是因为方法没用对

可变引用

只需要在引用符后面加一个mut,还有一个前提他是可变类型

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
  1. 首先声明s是可变类型

  2. 其次创建一个可变的引用&mut s和接受可变引用参数some_string: &mut String的函数

可变引用同时只能存在一个

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

这段代码就是错的,因为有两个可变引用

准确来讲,同一作用域下,特定数据只能由一个可变引用

很多时候,可以通过大括号划定作用域来解决编译不通过的问题

r

这样就是可以的

可变引用和不可变引用

let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);

上面这么写就有问题,错误如下

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
        // 无法借用可变 `s` 因为它已经被借用了不可变
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // 没问题
  |              -- immutable borrow occurs here 不可变借用发生在这里
5 |     let r2 = &s; // 没问题
6 |     let r3 = &mut s; // 大问题
  |              ^^^^^^ mutable borrow occurs here 可变借用发生在这里
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here 不可变借用在这里使用

这个原理就像读写功能:

&s是只读,可以有好多人一起读都不影响

而&mut s就是写,不可能很多人一起修改,也不能在其他人读的时候给他改了

所以,可变引用同时只能存在一个,且可变引用和不可变引用不能同时存在

悬垂引用

悬垂引用也叫悬垂指针,意思就是指针指向某个值后,这个值被释放掉了,但是指针仍然存在,它指向的内存可能不存在任何值或以及被其他变量重新使用了。

在Rust中编译器可以确保引用永远也不会变成悬垂状态

当获取数据的引用后,编译器可以确保数据不会再引用结束前被释放,要想释放数据,必须先停止引用的使用

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

这就是一个悬垂引用,Rust在编译时会直接报错

error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = 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
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

这个错误信指出:该函数返回了一个借用的值,但是以及找不到它所借用值的来源

来分析一下代码

fn dangle() -> &String { // dangle 返回一个字符串的引用

    let s = String::from("hello"); // s 是一个新字符串

    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!

因为s是在函数dangle内创建的,当dangle的代码执行完了以后,s会被释放,但是此时去尝试返回它的引用,这意味着这个引用指向的是一个无效的String

这里如果使用的是所有权转义就不会出现这个问题:

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

这样就不会报错了,String的所有权被转移给外面的调用者

借用规则总结

  • 同一时刻,只能拥有要么一个可变引用,要么任意多个不可变引用

  • 引用必须总是有效的