前言
这门语言准备作为开的新坑,日常记录学习进度
参考: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 runcargo 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!("{}", ®ion);
}
}
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:把栈比喻成地址簿可能有些不恰当,但是凑合理解)
堆和栈的性能区别
其实从条件上来看就能知道,栈上分配内存比堆上分配内存要快,因为入栈时操作系统不需要进行函数调用(或系统调用-更慢)来分配新的空间,只需要把新数据放到栈的顶部就行了;但是相比之下,堆就需要让操作系统先给他找一块足够存放数据的内存空间,接着做一些记录,为下次分配做准备,如果进程分配的内存不够的时候,还需要再去进行系统调用来申请更多内存;所以处理器在栈上分配数据比在堆上更高效
所有权与堆栈
当代码中调用一个函数的时候,传给函数的参数(包括可能指向堆上数据的指针和函数局部变量)的会被依次压入栈中,当函数调用结束之后,这些值再以入栈时相反的顺序被依次移除
因为在堆上缺乏组织性,因此跟踪这些数据什么时候被分配、什么时候被释放就极其重要,否则堆上的数据将产生内存泄漏,这些数据将永远不能被回收
所有权原则
Rust中每一个值都被一个变量所拥有,这个变量就被称为值的拥有者
一个值同时只能被一个变量所拥有
当所有者(变量)离开作用域范围的时候,这个值就会被丢掉(drop)
对于变量作用域和Go语言一样,就不再过多赘述
string类型
这里讲的主要是在rust语言里string和其他语言里的区别
首先是字符串字面量,注意这个字面量,也就是let s = "hello";
它的类型是&str,和其他语言不同的是:他是被硬编码在程序里的(放在只读的常量区),是不可变的
这代表他是写死在程序里,当字符串需要在运行时,通过用户动态输入后存储到内存中的情况,就不能使用它
Rust还另外提供了一种可以增长的、堆上分配的字符串类型String,它和上面那个不同的是:
通过
String::from("hello")创建(::是一种调用操作符,这里表示调用string类型中的from关联函数)可以动态变长,比如
push_str(", world!")因为是存放在堆上,所以它可以存一些运行时才知道的内容,就比如用户输入
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;就要分情况讨论
拷贝String和存储在堆上的字节数据 如果这条语句是拷贝所有数据(深拷贝),那无论是String本身还是底层的堆上数据,全部会被拷贝,也就是栈上的那些数据以及堆上的hello字节,非常影响性能
只拷贝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的后续使用
注意两点:
不需要和上面章节一样先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁
可以看到函数里参数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");
}首先声明s是可变类型
其次创建一个可变的引用
&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的所有权被转移给外面的调用者
借用规则总结
同一时刻,只能拥有要么一个可变引用,要么任意多个不可变引用
引用必须总是有效的