理解Rust的所有权

什么是所有权

Rust的所有权,是一个跨时代的理念,是内存管理的第二次革命。
较低级的语言依赖程序员分配和释放内存,一不小心就会出现空指针、野指针破坏内存;较高级的语言使用垃圾回收的机制管理内存,在程序运行时不断地寻找不再使用的内存,虽然安全,却加重了程序的负担;Rust的所有权理念横空出世,通过所有权系统管理内存, 编译器在编译时会根据一系列的规则进行检查,在运行时,所有权系统的任何功能都不会减慢程序,把安全的内存管理推向了0开销的新时代。

Rust的所有权并不难理解,它有且只有如下三条规则:

  1. Rust中的每一个值都有一个所有者(owner)的变量;
  2. 一个值有且只有一个所有者;
  3. 当所有者离开作用域时这个值将被丢弃。

真正难以理解的是内存的使用情况。

栈和堆的回忆

在学习C/C++时,老师经常出某某变量被分配在栈上还是堆上的题目,几乎每次都有很大一批同学在这种题目上失手。
栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈是一种后进先出(LIFO)的数据结构,栈中的所有数据都必须占用已知且固定的大小。堆就好理解了,它是一个没有组织的结构,你想怎么使用就怎么使用,只要堆够大,你就可以申请一段内存空间,然后把这段内存标记为已使用,并得到指向这段内存开头的指针;当你不再使用时,再将这段内存标记回未使用。
当声明了一个指针但并没有分配空间时,这个指针是空指针;当内存已经标记回未使用,而指针还依然指向这段空间时,这个指针就是野指针。
回到刚才说的题目,其实分辨起来很容易,动态分配的变量就是在堆上,其他的都在栈上。
栈是一个成熟的结构,基本不会引发内存的问题,而没有组织的堆却很容易引发内存问题。垃圾回收和所有权,都是为了解决堆的内存管理问题。

拷贝(Copy)、移动(Move)与克隆(Clone)

Rust把栈上的内存复制,称为拷贝(Copy)。看下面的代码:

fn main() 
{
    let x = 5;
    let y = x;
    println!("x:{} y:{}", x, y);
}

整数的内存是分配在栈上,Rust将x的值拷贝到的y变量中。
而对于字符串,看上去一样的代码却是不一样的结果:

fn main() 
{
    let x = String::from("hello");
    let y = x;
    println!("x:{} y:{}", x, y);
}

看似简单的代码却不能通过编译:

error[E0382]: borrow of moved value: `x`
 --> src\main.rs:5:27
  |
3 |     let x = String::from("hello");
  |         - move occurs because `x` has type `std::string::String`, which does not implement the `Copy` trait
4 |     let y = x;
  |             - value moved here
5 |     println!("x:{} y:{}", x, y);
  |                           ^ value borrowed here after move

error: aborting due to previous error

这里面提到了Copy trait,暂且不管,大概意思是值从x移动到了y,移动之后,x不能用了。
为什么会这样呢?
Rust中String类型变量的内存是在堆上分配的,执行let x = String::from(“hello”)时,Rust在堆上分配了内存,并将其绑定到x变量。String类型有三个成员变量(先沿用C++的概念),一个是指向分配的内存的指针,一个是内存的长度,另一个是内存使用了的长度。
当执行let y=x时,Rust将x的这三个成员变量拷贝给了y,但是并没有将字符串占用的内存拷贝给y。这类似于其他语言的浅拷贝,只不过Rust还多做了一步,让原来的x失效。这在Rust中被称为移动(Move)。
要牢记下面一句话:Rust永远也不会自动创建数据的 “深拷贝”。

要想进行深拷贝,需要使用克隆(Clone):

fn main() 
{
    let x = String::from("hello");
    let y = x.clone();
    println!("x:{} y:{}", x, y);
}

这段代码就可以正常的运行了。
拷贝、移动和克隆,不仅仅发生在赋值时,当调用函数传递参数或从函数接收返回值时,它们也会发生,我自己写猜猜看游戏时,就是在把String当作参数传递到其他函数内时发生了移动。如下面的代码:

fn main() 
{
    let x = String::from("hello");
    print(x);
    println!("x:{}", x);
}

fn print(s:String)
{
    println!("{}", s)
}

编译器会报错:

error[E0382]: borrow of moved value: `x`
 --> src\main.rs:5:22
  |
3 |     let x = String::from("hello");
  |         - move occurs because `x` has type `std::string::String`, which does not implement the `Copy` trait
4 |     print(x);
  |           - value moved here
5 |     println!("x:{}", x);
  |                      ^ value borrowed here after move

error: aborting due to previous error

调用print函数时,x的值移动到了print函数内,而print函数执行完成后,s离开了作用域,内存被销毁了。再使用x变量就会出现问题。
在其它语言中,比如Python,并不容易分清哪些是浅拷贝,哪些是深拷贝,而Rust则将其严格的区分开来,当你看到或用到clone时,一定要引用注意,这么做是耗费资源的,一定要这么做吗?

引用与借用

当移动发生时,内存的所有权发生了转移。那么有没有办法不转移所有权呢?答案是肯定的,就是通过引用。将我前面出错的例子修改为引用:

fn main() 
{
    let x = String::from("hello");
    print(&x);
    println!("x:{}", x);
}

fn print(s:&String)
{
    println!("{}", s)
}

这个print函数的参数不再是字符串,而是字符串的引用,这样,内存的所有权依然是属于x,而不是s,当s离开作用域后,内存不会被销毁,x还可以继续使用。
Rust将获取引用作为函数的参数的方式叫做借用(borrowing),这很容易被理解,张三有5块钱,他把钱给李四了,钱的所有权从张三转移到李四,和张三没有关系了;而如果李四是向张三借钱,钱的所有权不会发生转移,还是张三的,只是李四用了一下而已。
引用也具有可变性,同样,Rust默认是不可变的。要想让引用可变,双方都需要添加mut关键字:

fn main() 
{
    let mut x = String::from("hello");
    print(&mut x);
    println!("x:{}", x);
}

fn print(s:&mut String)
{
    s.push_str(" world.");
    println!("{}", s)
}

从执行结果中可以看出,print函数中改变了字符串的值,同时,main函数中x的值也同样发生了改变,因为他们是同一段内存地址。
我能把钱同时借给两个人吗?可以,但同时只能有一个人花。Rust限制可变引用的数量,同时只能有一个可变引用:

fn main() 
{
    let x = String::from("hello");
    let y = &x;
    let z = &x;
    println!("x:{} y:{} z:{}", x, y, z);
}

这是没有问题的,但换成可变引用:

fn main() 
{
    let mut x = String::from("hello");
    let y = &mut x;
    let z = &mut x;
    println!("x:{} y:{} z:{}", x, y, z);
}

编译器就会报错:

error[E0499]: cannot borrow `x` as mutable more than once at a time
 --> src\main.rs:5:13
  |
4 |     let y = &mut x;
  |             ------ first mutable borrow occurs here
5 |     let z = &mut x;
  |             ^^^^^^ second mutable borrow occurs here
6 |     println!("x:{} y:{} z:{}", x, y, z);
  |                                   - first borrow later used here

那好,既然不能同时有两个,那就去掉一个,改成:

fn main() 
{
    let mut x = String::from("hello");
    let y = &x;
    let z = &mut x;
    println!("x:{} y:{} z:{}", x, y, z);
}

编译器还是会无情的亮红灯:

error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
 --> src\main.rs:5:13
  |
4 |     let y = &x;
  |             -- immutable borrow occurs here
5 |     let z = &mut x;
  |             ^^^^^^ mutable borrow occurs here
6 |     println!("x:{} y:{} z:{}", x, y, z);
  |                                   - immutable borrow later used here

难道是只要有可变引用就不能再使用?也没有这么绝对,再看一面的代码:

fn main() 
{
    let mut x = String::from("hello");
    let y = &x;
    println!("x:{} y:{}", x, y);
    let z = &mut x;
    z.push_str(" world");
    println!("z:{}", z);
}

怎么又没有问题了呢?Rust的编译器还是很智能的,只要不可变的引用在可变的引用之后不再使用,那么就不会出现数据竞争的问题,那么,自然就可以编译通过了。

悬垂引用

C/C++中,常见的指针错误还有野指针,即指针指向的内存已经被释放,继续使用该指针会引发错误。这在Rust中叫做悬垂引用(Dangling References)。当然,你不需要担心发生了悬垂引用怎么办,因为Rust的编译器会检查到悬垂引用:

fn main() 
{
    let x = get_string();
    println!("x:{}", x);
}

fn get_string()->&String
{
    &String::from("hello")
}

先来分析这段代码,main函数中调用了get_string函数获取一个字符串的引用,因为这个字符串是在get_string中创建的,函数结束时这个字符串的生命周期随之结束,引用返回给x后,就发生了悬垂引用。Rust的编译器会检查到这个错误:

error[E0106]: missing lifetime specifier
 --> src\main.rs:7:18
  |
7 | fn get_string()->&String
  |                  ^ help: consider giving it a 'static lifetime: `&'static`
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

error: aborting due to previous error

与想像中的不同,错误并不是指出发生了悬垂引用,而是说缺少了生命周期说明。
Rust有一个独特的语法,叫做生命周期注解,这在其他语言中没有的。不过生命周期注解并不能解决上面的悬垂引用问题,Rust的编译器不会允许悬垂引用的发生。
生命周期注解大多数情况下都是与泛型结合使用,这里简单记录一下生命周期注解的语法,学习泛型的时候再详细记录。
生命周期参数名称以单引号开头, 其名称通常全是小写,如:

&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

生命周期注解大多数情况下都是与泛型结合使用,这里简单记录一下生命周期注解的语法,学习泛型的时候再详细记录。
话说回来,上面的悬垂引用的问题到底如何解决呢?答案是不使用引用:

fn main() 
{
    let x = get_string();
    println!("x:{}", x);
}

fn get_string()->String
{
    String::from("hello")
}

get_string函数结束前,字符串的所有权被转移到x变量中,因此字符串的内存不会被销毁,可以继续被使用。

重新认识切片

Rust基础概念一章中,已经记录了切片的使用方法,在理解了Rust的所有权以后,有必要重新认识一下切片了,因为,它是一个没有所有权的类型,其本质还是引用,只不过引用的是数据的一部分。
回顾一下切片的使用:

fn main() {
    let a = [1,2,3,4,5,6,7,8,9,0];
    let s = &a[2..8];
    println!("{}", s[0]);
}

当时学习的时候有没有这样的疑问,为什么a[2…8]前面要有‘&’符号?现在应该能理解了吧。
对于字符串的切片,同样适用前面的引用规则,不过有点些特殊的地方:即便是对字符串不同区域的引用,也不能同时拥有两个可变引用。如:

fn main() 
{
    let mut x = String::from("hello world");
    let frist = &mut x[..5];
    let second = &mut x[6..];
    println!("frist:{} second:{}", frist, second);
}

frist是"hello",second是"world",这两个切片是不重叠的。但编译器还是会报错:

error[E0499]: cannot borrow `x` as mutable more than once at a time
 --> src\main.rs:5:23
  |
4 |     let frist = &mut x[..5];
  |                      - first mutable borrow occurs here
5 |     let second = &mut x[6..];
  |                       ^ second mutable borrow occurs here
6 |     println!("frist:{} second:{}", frist, second);
  |                                    ----- first borrow later used here

error: aborting due to previous error

Rust基础概念记录字符串时,我写到:

在Rust中,字符串被分为两种,str和String

特此更正一下,应该是String和&str,原文已经进行了修正,希望没有对朋友们造成误导。
&str就是字符串的字面值,事实上,它就是一个切片,是对字符串的所有字符引用的切片。
这个字符串编译完后保存在可执行文件中,执行时其内存在代码区,既不在栈里,也不在堆里。因此,它是不可变的。要想操作它,需要先将其转换为String,String的内存保存在堆里,就可以任意操作了。

回到前面print函数的例子:

fn main() 
{
    let x = String::from("hello");
    print(&x);
}

fn print(s:&String)
{
    println!("{}", s)
}

将String类型的引用作为函数的参数,它可以正常工作,但是,如果传入的是字符串字面值:

fn main() 
{
    print("hello");
}

fn print(s:&String)
{
    println!("{}", s)
}

严格的编译器又开始啰嗦了:

error[E0308]: mismatched types
 --> src\main.rs:3:11
  |
3 |     print("hello");
  |           ^^^^^^^ expected struct `std::string::String`, found str
  |
  = note: expected type `&std::string::String`
             found type `&'static str`

error: aborting due to previous error

此时,我们需要传入的参数类型应该是&str:

fn main() 
{
    print("hello");
}

fn print(s:&str)
{
    println!("{}", s)
}

确实,它工作的很好,那我再传入String会怎么样?

fn main() 
{
    print("hello");

    let x = String::from("world");
    print(&x);
}

fn print(s:&str)
{
    println!("{}", s)
}

什么?它竟然还能工作?这又是为什么?
上面代码中的print(&x),是对整个字符串的引用,它与print(&x[…]);是等价的,因此,在需要将字符串作为函数参数时,最好的方式是用&str作为参数,这样可以通用。如果函数中需要对字符串进行其它操作,可以使用to_string()将参数转换为String类型进行处理。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值