github地址:https://github.com/bradyjoestar/rustnotes(欢迎star!)
pdf下载链接:https://github.com/bradyjoestar/rustnotes/blob/master/Rust%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.pdf
参考:
https://rustcc.gitbooks.io/rustprimer/content/ 《RustPrimer》
https://kaisery.github.io/trpl-zh-cn/ 《Rust程序设计语言-简体中文版》
一个C语言的例子:
int* foo() {
int a; // 变量a的作用域开始
a = 100;
char *c = "xyz"; // 变量c的作用域开始
return &a;
} // 变量a和c的作用域结束
尽管可以编译通过,但这是一段非常糟糕的代码,变量a和c都是局部变量,函数结束后将局部变量a的地址返回,但局部变量a存在栈中,在离开作用域后,局部变量所申请的栈上内存都会被系统回收,从而造成了Dangling Pointer的问题。这是一个非常典型的内存安全问题。很多编程语言都存在类似这样的内存安全问题。
再来看变量c,c的值是常量字符串,存储于常量区,可能这个函数我们只调用了一次,我们可能不再想使用这个字符串,但xyz只有当整个程序结束后系统才能回收这片内存,这点让程序员是不是也很无奈?
所以,内存安全和内存管理通常是程序员眼中的两大头疼问题。令人兴奋的是,Rust却不再让你担心内存安全问题,也不用再操心内存管理的麻烦,那Rust是如何做到这一点的?通过所有权。
3.1 所有权
3.1.1 绑定
首先必须强调下,准确地说Rust中并没有变量这一概念,而应该称为标识符,目标资源(内存,存放value)绑定到这个标识符。
{
let x: i32; // 标识符x, 没有绑定任何资源
let y: i32 = 100; // 标识符y,绑定资源100
}
Rust并不会像其他语言一样可以为变量默认初始化值,Rust明确规定变量的初始值必须由程序员自己决定。
上述代码中,let关键字并不只是声明变量的意思,它还有一层特殊且重要的概念-绑定。通俗的讲,let关键字可以把一个标识符和一段内存区域做“绑定”,绑定后,这段内存就被这个标识符所拥有,这个标识符也成为这段内存的唯一所有者。
3.1.2 作用域
rust有着和其它语言类型的定义作用域的规则。
像C语言一样,在局部变量离开作用域后,变量随即会被销毁;但不同是,Rust会连同变量绑定的内存,不管是否为常量字符串,连同所有者变量一起被销毁释放。
3.1.3 移动语义
在Rust中,和“绑定”概念相辅相成的另一个机制就是“转移move所有权”,意思是,可以把资源的所有权(ownership)从一个绑定转移(move)成另一个绑定,这个操作同样通过let关键字完成,和绑定不同的是,=两边的左值和右值均为两个标识符:
语法:
let 标识符A = 标识符B; // 把“B”绑定资源的所有权转移给“A”
move前后的内存示意如下:
Before move:
a <=> 内存(地址:A,内容:"xyz")
After move:
a
b <=> 内存(地址:A,内容:"xyz")
move后,如果变量A和变量B离开作用域,所对应的内存会不会造成“Double Free”的问题?答案是否定的,Rust规定,只有资源的所有者销毁后才释放内存,而无论这个资源是否被多次move,同一时刻只有一个owner,所以该资源的内存也只会被free一次。
3.1.4 Copy特性
举例如下:
let a: i32 = 100;
let b = a;
println!("{}", a);
编译确实可以通过,输出为100。这是为什么呢,是不是跟move小节里的结论相悖了? 其实不然,这其实是根据变量类型是否实现Copy特性决定的。对于实现Copy特性的变量,在move时会拷贝资源到新内存区域,并把新内存区域的资源binding为b。特性有点类似于interface,但是在rust中必须显式实现。
Before move:
a <=> 内存(地址:A,内容:100)
After move:
a <=> 内存(地址:A,内容:100)
b <=> 内存(地址:B,内容:100)
在Rust中,基本数据类型(Primitive Types)均实现了Copy特性,包括i8, i16, i32, i64, usize, u8, u16, u32, u64, f32, f64, (), bool, char等等。
3.1.5 浅拷贝与深拷贝
对于基本数据类型来说,“深拷贝”和“浅拷贝“产生的效果相同。对于引用对象类型来说,”浅拷贝“更像仅仅拷贝了对象的内存地址。 如果我们想实现对String的”深拷贝“怎么办? 可以直接调用String的Clone特性实现对内存的值拷贝而不是简单的地址拷贝。
3.1.6 高级copy
一旦一种类型实现了Copy特性,这就意味着这种类型可以通过的简单的位(bits)拷贝实现拷贝。从前面知识我们知道“绑定”存在move语义(所有权转移),但是,一旦这种类型实现了Copy特性,会先拷贝内容到新内存区域,然后把新内存区域和这个标识符做绑定。
哪些情况下我们自定义的类型(如某个Struct等)可以实现Copy特性? 只要这种类型的属性类型都实现了Copy特性,那么这个类型就可以实现Copy特性。 例如:
struct Foo { //可实现Copy特性
a: i32,
b: bool,
}
struct Bar { //不可实现Copy特性
l: Vec<i32>,
}
因为Foo的属性a和b的类型i32和bool均实现了Copy特性,所以Foo也是可以实现Copy特性的。但对于Bar来说,它的属性l是Vec<T>类型,这种类型并没有实现Copy特性,所以Bar也是无法实现Copy特性的。
那么我们如何来实现Copy特性呢?有两种方式可以实现。
3.1.6.1.通过derive让Rust编译器自动实现
#[derive(Copy, Clone)]
struct Foo {
a: i32,
b: bool,
}
3.1.6.2 手动实现 不推荐 会进入unsafe rust
3.1.7 Copy trait与Clone trait
Copy内部没有方法,Clone内部有两个方法。
1.Copy trait 是给编译器用的,告诉编译器这个类型默认采用 copy 语义,而不是 move 语义。Clone trait 是给程序员用的,我们必须手动调用clone方法,它才能发挥作用。
2.Copy trait不是你想实现就实现,它对类型是有要求的,有些类型就不可能 impl Copy。Clone trait 没有什么前提条件,任何类型都可以实现(unsized 类型除外)。
3.Copy trait规定了这个类型在执行变量绑定、函数参数传递、函数返回等场景下的操作方式。即这个类型在这种场景下,必然执行的是“简单内存拷贝”操作,这是由编译器保证的,程序员无法控制。Clone trait 里面的 clone 方法究竟会执行什么操作,则是取决于程序员自己写的逻辑。一般情况下,clone 方法应该执行一个“深拷贝”操作,但这不是强制的,如果你愿意,也可以在里面启动一个人工智能程序,都是有可能的。
5.如果你确实需要Clone trait执行“深拷贝”操作,编译器帮我们提供了一个工具,我们可以在一个类型上添加#[derive(Clone)],来让编译器帮我们自动生成那些重复的代码。
正因为如此,在希望让一个类型具有 Copy 性质的时候,一般使用 #[derive(Copy, Clone)] 这种方式,这种情况下它们俩最好一起出现,避免手工实现 Clone 导致错误。
3.1.8 高级move
move关键字常用在闭包中,强制闭包获取所有权。
fn main() {
let mut x: String = String::from("abc");
let mut some_closure = move |c: char| x.push(c);
let y = some_closure('d');
println!("x={:?}", x);
}
上述代码会报错。
这是因为move关键字,会把闭包中的外部变量的所有权move到包体内,发生了所有权转移的问题,所以println访问x会如上错误。如果我们去掉println就可以编译通过。
那么,如果我们想在包体外依然访问x,即x不失去所有权,怎么办?
fn main() {
let mut x: String = String::from("abc");
{
let mut some_closure = |c: char| x.push(c);
some_closure('d');
}
println!("x={:?}", x); //成功打印:x="abcd"
}
我们只是去掉了move,去掉move后,包体内就会对x进行了可变借用,而不是“剥夺”x的所有权,细心的同学还注意到我们在前后还加了{}大括号作用域,是为了作用域结束后让可变借用失效,这样println才可以成功访问并打印我们期待的内容。
最新的版本不加大括号也可以的?但是为了可读加上大括号比较好。尽可能满足作用域内部的规则。
具体内容需要查看闭包的可变借用。
3.2 引用和借用
所有权系统允许我们通过“Borrowing”的方式达到这个目的。这个机制非常像其他编程语言中的“读写锁”,即同一时刻,只能拥有一个“写锁”,或只能拥有多个“读锁”,不允许“写锁”和“读锁”在同一时刻同时出现。当然这也是数据读写过程中保障一致性的典型做法。只不过Rust是在编译中完成这个(Borrowing)检查的,而不是在运行时,这也就是为什么其他语言程序在运行过程中,容易出现死锁或者野指针的问题。
通过&符号完成Borrowing:
fn main() {
let x: Vec<i32> = vec!(1i32, 2, 3);
let y = &x;
println!("x={:?}, y={:?}", x, y);
}
Borrowing(&x)并不会发生所有权moved,所以println可以同时访问x和y。 通过引用,就可以对普通类型完成修改。
fn main() {
let mut x: i32 = 100;
{
let y: &mut i32 = &mut x;
*y += 2;
}
println!("{}", x);
}
y在大括号结束后会释放掉。
& 符号就是 引用,它们允许你使用值但不获取其所有权。将获取引用作为函数参数称为 借用(borrowing)。
3.2.1 借用和引用的规则
1.同一作用域,特定数据最多只有一个可变借用(&mut T),或者2。
2.同一作用域,特定数据可有0个或多个不可变借用(&T),但不能有任何可变借用。
3.借用在离开作用域后释放。
4.在可变借用释放前不可访问源变量。
3.2.2 引用的可变性
Borrowing也分“不可变借用”(默认,&T)和“可变借用”(&mut T)。
顾名思义,“不可变借用”是只读的,不可更新被引用的内容。
fn main() {
//源变量x可变性
let mut x: Vec<i32> = vec!(1i32, 2, 3);
//只能有一个可变借用
let y = &mut x;
// let z = &mut x; //错误
y.push(100);
//ok
println!("{:?}", y);
//错误,可变借用未释放,源变量不可访问
// println!("{:?}", x);
} //y在此处销毁
另外一个例子:
fn main() {
let mut x: Vec<i32> = vec!(1i32, 2, 3);
//更新数组
//push中对数组进行了可变借用,并在push函数退出时销毁这个借用
x.push(10);
{
//可变借用1
let mut y = &mut x;
y.push(100);
//可变借用2,注意:此处是对y的借用,不可再对x进行借用,
//因为y在此时依然存活。
let z = &mut y;
z.push(1000);
println!("{:?}", z); //打印: [1, 2, 3, 10, 100, 1000]
} //y和z在此处被销毁,并释放借用。
//访问x正常
println!("{:?}", x); //打印: [1, 2, 3, 10, 100, 1000]
}
3.2.3 总结
1.借用不改变内存的所有者(Owner),借用只是对源内存的临时引用。
2.在借用周期内,借用方可以读写这块内存,所有者被禁止读写内存;且所有者保证在有“借用”存在的情况下,不会释放或转移内存。
3.失去所有权的变量不可以被借用(访问)。
4.在租借期内,内存所有者保证不会释放/转移/可变租借这块内存,但如果是在非可变租借的情况下,所有者是允许继续非可变租借出去的。
5.借用周期满后,所有者收回读写权限。
6.借用周期小于被借用者(所有者)的生命周期。
3.3 生命周期
几个概念:
1.Owner: 资源的所有者 a
2.Borrower: 资源的借用者 x
3.Scope: 作用域,资源被借用/引用的有效期
无论是资源的所有者还是资源的借用/引用,都存在在一个有效的存活时间或区间,这个时间区间称为生命周期, 也可以直接以Scope作用域去理解。
例子:
fn main() {
let a = 100_i32;
{
let x = &a;
} // x 作用域结束
println!("{}", x);
}
分析:借用者 x 的生命周期是资源所有者 a 的生命周期的子集。但是 x 的生命周期在第一个 }时结束并销毁,在接下来的 println! 中再次访问便会发生严重的错误。
3.3.1 隐式lifetime
我们经常会遇到参数或者返回值为引用类型的函数:
fn foo(x: &str) -> &str {
x
}
foo 函数仅仅接受一个 &str 类型的参数(x为对某个string类型资源Something的借用),并返回对资源Something的一个新的借用。
实际上,上面函数包含隐性的生命周期命名,这是由编译器自动推导的,相当于:
fn foo<'a>(x: &'a str) -> &'a str {
x
}
3.4 高级所有权
前面三小节未大量涉及到关于所有权中的比较高级用法。
3.4.1.函数传递参数和返回参数类似于let语句
在rust中,函数是存放在函数栈,为了更为快速的运行。
函数输入参数的传递和返回参数的赋值类似于let语句,都看传递的参数和返回的参数是否实现了copy trait。
如果实现了copy trait,那么就不会夺走它的所有权,标识符在函数外部还可以继续访问。
如果没有实现copy trait,那么它的所有权都会被夺走。
需要说明的是,从结果上来看,引用是值传递,和实现了copy trait的表征相同,同样可以外部继续使用。
举例而言:
pub fn test(){
let a = vec![1,2,3,4,5];
print_vec(a);
//error[E0382]: borrow of moved value: `a`
// --> src/fn_params.rs:6:21
// |
//2 | let a = vec![1,2,3,4,5];
// | - move occurs because `a` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
//3 |
//4 | print_vec(a);
// | - value moved here
//5 |
//6 | println!("{:?}",a);
// | ^ value borrowed here after move
// println!("{:?}",a);
{
let a = vec![1,2,3,4,5];
let b = &a;
print_vecs(b);
//no error
println!("{:?}",b);
}
}
fn print_vec(a: Vec<i32>){
println!("{:?}",a);
}
fn print_vecs(a:&Vec<i32>){
println!("{:?}",a)
}
从上面可以看出,以vec!为函数参数则发生了move,在后面无法使用。
以&vec!为函数参数,后续仍然可以使用&vec!。
错误提示中也是说明根据是否实现copy trait来决定是否进行所有权的转移。
3.4.2 涉及到函数和结构体的借用检查器
在rust中引入引用后,我们需要使用引入借用检查器来保证引用的生命周期不会超过所有权的生命周期。
Rust 编译器有一个 借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。避免出现类似C的悬挂指针等问题。借用检查器是在编译阶段进行工作的,将所有的无效借用识别出来。
大部分情况下,借用检查器可以正常工作,对于某些特殊情况,借用检查器无法识别,需要由开发人员显示标注生命周期。
主要分为以下三种情况:
1.函数定义中的生命周期注解
2.结构体定义中的生命周期注解
3.方法定义中的生命周期注解
上述三种类型的生命周期注解主要用来为借用检查器提供帮助,查看使用到它们的地方是否满足借用的生命周期规则,有没有悬挂指针的问题。
3.4.2.1 函数定义中的生命周期注解
举例而言:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
rust程序设计语言 简体中文版已经描述的非常到位了。
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,让 Rust 自身分析出参数或返回值的生命周期几乎是不可能的。这些生命周期在每次函数被调用时都可能不同。这也就是为什么我们需要手动标记生命周期。
当具体的引用被传递给 longest 时,被 'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在x和y中较短的那个生命周期结束之前保持有效。
要推导Lifetime是否合法,先明确两点:
1.输出值(也称为返回值)依赖哪些输入值
2.输入值的Lifetime大于或等于(可能依赖的输出值)的Lifetime (准确来说:子集,而不是大于或等于)
Lifetime推导公式: 当输出值R依赖输入值X Y Z ...,当且仅当输出值的Lifetime为所有输入值的Lifetime交集的子集时,生命周期合法。
例子1:
fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
if true {
x
} else {
y
}
}
因为返回值同时依赖输入参数x和y,所以
Lifetime(返回值) ⊆ ( Lifetime(x) ∩ Lifetime(y) )
即:
'a ⊆ ('a ∩ 'a) // 成立
例子2:
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
if true {
x
} else {
y
}
}
因为返回值同时依赖x和y,所以
Lifetime(返回值) ⊆ ( Lifetime(x) ∩ Lifetime(y) )
即:
'a ⊆ ('a ∩ 'b) //不成立
很显然,上面我们根本没法保证成立。
所以,这种情况下,我们可以显式地告诉编译器'b比'a长('a是'b的子集),只需要在定义Lifetime的时候, 在'b的后面加上: 'a, 意思是'b比'a长,'a是'b的子集:
fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if true {
x
} else {
y
}
}
这里我们根据公式继续推导:
条件:Lifetime(x) ⊆ Lifetime(y)
推导:Lifetime(返回值) ⊆ ( Lifetime(x) ∩ Lifetime(y) )
即:
条件: 'a ⊆ 'b
推导:'a ⊆ ('a ∩ 'b) // 成立
上面是成立的,所以可以编译通过。
3.4.2.2 结构体中的生命周期
在struct中Lifetime同样重要。关于rust中struct使用请查看第四章。
我们来定义一个Person结构体。
struct Person {
age: &u8,
}
编译时我们会得到一个error:
<anon>:2:8: 2:12 error: missing lifetime specifier [E0106]
<anon>:2 age: &str,
之所以会报错,这是因为Rust要确保Person的Lifetime不会比它的age借用长,不然会出现Dangling Pointer的严重内存问题。而在结构体中我们又没有进行指定。所以我们需要为age借用声明Lifetime:
struct Person<'a> {
age: &'a u8,
}
不需要对Person后面的<'a>感到疑惑,这里的'a并不是指Person这个struct的Lifetime,仅仅是一个泛型参数而已,struct可以有多个Lifetime参数用来约束不同的field,实际的Lifetime应该是所有fieldLifetime交集的子集。
fn main() {
let x = 20_u8;
let stormgbs = Person {
age: &x,
};
}
这里,生命周期/Scope的示意图如下:
{ x stormgbs * }
所有者 x: |________________________|
所有者 stormgbs: |_______________| 'a
借用者 stormgbs.age: |_______________| stormgbs.age = &x
3.4.2.3 结构体方法定义的生命周期
既然<'a>作为Person的泛型参数,所以在为Person实现方法时也需要加上<'a>,不然:
impl Person {
fn print_age(&self) {
println!("Person.age = {}", self.age);
}
}
报错:
<anon>:5:6: 5:12 error: wrong number of lifetime parameters: expected 1, found 0 [E0107]
<anon>:5 impl Person {
^~~~~~
正确的做法是:(age是有生命周期的)
impl<'a> Person<'a> {
fn print_age(&self) {
println!("Person.age = {}", self.age);
}
}
这样加上<'a>后就可以了。读者可能会疑问,为什么print_age中不需要加上'a?这是个好问题。因为print_age的输出参数为(),也就是可以不依赖任何输入参数, 所以编译器此时可以不必关心和推导Lifetime。即使是fn print_age(&self, other_age: &i32) {...}也可以编译通过。
如果Person的方法存在输出值(借用)呢?
impl<'a> Person<'a> {
fn get_age(&self) -> &u8 {
self.age
}
}
get_age方法的输出值依赖一个输入值&self,这种情况下,Rust编译器可以自动推导为:
impl<'a> Person<'a> {
fn get_age(&'a self) -> &'a u8 {
self.age
}
}
如果输出值(借用)依赖了多个输入值呢?
impl<'a, 'b> Person<'a> {
fn get_max_age(&'a self, p: &'a Person) -> &'a u8 {
if self.age > p.age {
self.age
} else {
p.age
}
}
}
类似之前的Lifetime推导章节,当返回值(借用)依赖多个输入值时,需显示声明Lifetime。和函数Lifetime同理。
其他无论在函数还是在struct中,甚至在enum中,Lifetime理论知识都是一样的。希望大家可以慢慢体会和吸收,做到举一反三。