深入解析rust中的所有权规则 - 掘金

从一段代码开始

在讲所有权与租借之前,我们先来看一段代码:

fn print_str(a: String) {
    println!("the input string is {}", a);
}
fn print_len_str(a: String) {
    println!("the length of input string is {}", a.len());
}

fn main() {
    let a = String::from("hello rust");
    print_str(a);
    print_len_str(a);
}
复制代码

这是一段非常简单的代码,我们先声明了一个不可变变量a,之后依次把它传给print_str和print_len_str方法,分别输出a的内容和长度。我不敢说100%,至少在我已知的语言中,这样调用绝对是不会有任何的问题的,不出意外你应该可以在控制台看到量看输出。但是往往,我们说不出意外的时候,意外就来了。

image.png

有点懵,也有点不知所以。为啥呢?

这就要从rust的内存管理机制说起了。

rust中的所有权(ownership)

之前我们提到过,rust为了获得和c一样的性能和安全的内存管理做了很多令人发指的设计,而所有权就是其中之一。

内存管理是任何编程语言都绕不过去的话题。通常比较常见的内存管理方式有两种:

  1. 垃圾回收机制,就像js,系统在通过某种方式自动管理内存,在特定的时候回收内存;
  2. 手动管理,就像C++。

垃圾回收机制和手动管理各有利弊,垃圾回收机制通常更加安全,但是在内存回收的时候会带来额外的性能开销。手动管理没有垃圾回收,自然也就没有额外的性能开销,但是问题是,手动管理对程序员要求很高,有时候会很痛苦。

但是rust,秉承着小孩子才做选择,大人全都要的理念,变态性的使用了第三种内存管理方案:所有权管理

所有权规则

要理解所有权,你首先要对内存有一个初步的了解。

let a = "rust"
复制代码

当我们执行上面这行代码的时候,rust会先在内存中申请一个区域用来存储"rust"这个字符串,变量a就相当于拥有了这块内存的所有权。在a不在拥有这块变量的所有权的时候,内存就会被回收掉。

看上去和垃圾回收机制也差不多是不是?但是你接着往下看。

在rust中,所有权规则主要有以下三条:

  1. rust中的每个值都对应一个变量,这个值就是变量的所有者。
  2. 一个值同时只能有一个所有者。
  3. 当所有者不在程序运行范围时,该值将被删除

第一条规则很好理解,之前的代码中,a就是“rust”这个值的所有者,因此对其拥有所有权。

第二和第三条规则是我们理解所有权规则的关键,我们接着看代码:

let a = String::from("rust");
let b = a;
println!("{}", a)
复制代码

这行代码看上去没什么问题,但是在实际运行的时候你将会收到一个异常,原因就是它违背了所有权规则中的第二条:一个值同时只能有一个所有者。 同样的,我们在最开始的例子运行失败,也是因为它违背了这条原则。

打起来精神来,不要划水了,接下来的内容很重要,将会是你能否完全掌握rust所有权规则的关键。

总所周知,变量在内存中有两种存储方式:堆和栈。一般来讲基本数据类型(number、string、boolean等)的值会存在栈中,否则(例如String对象,不清楚string和String区别的可以在评论区提问,相信一定会有人能给你解答)就存在堆中,然后把这个变量在堆中所在的位置(一般是一个整型值,属于简单数据类型)存在栈中。上例中,a的类型是String,不属于简单数据类型,因此它的值是存在堆中的。当你把a赋值给b的时候,并不是直接把值赋给b,而是把值(简单来讲就是rust这个字符串)所在堆中对应的内存地址等一系列赋给b(这里其实也不很准确),也就是说,这时候a和b被指向了相同的一块内存区域,但是根据规则2,一块内存区域同时只能拥有一个所有者,因此,编译器给你异常提醒。

但是如果a不是一个复杂数据类型,情况又会怎么样呢?

例如:

let a = "rust";
let b = a;
println!("{}", a)
复制代码

你会发现这次编译没有报错,原因是什么呢?因为这是的a类型是string,是一个简单的数据类型。简单的数据类型值会被存储在栈中,a拥有的就是这变量的值而不是堆地址等信息,所以当把a赋值给b的时候,实际是把a代表的值赋值给了b。什么意思呢?就是rust把a的值重新复制了一份放在了栈另外的位置,a和b实际上拥有的是不同的内存区域,所以依然符合所有权规则2。

那么当a是复杂数据类型的时候不采用直接复制一份值给b呢?答案是:没法复制。当a是复杂类型的是,a实际拥有的是堆地址信息,复制一份堆地址信息并没有任何意义,因为实际指向堆中的区域还是相同的。

在了解了所有权规则2之后,我们在来看看为什么rust要做这套设计呢。试想一下,没有规则2,那么意味着a和b的任何一个变量在被回收的时候,a和b对应值所在的内存区域就会被回收,这会带来两个问题:

  1. a和b任何一个失效都会导致这个变量失效,因为内存已经被回收了。
  2. a和b对应的内存区域可能会被回收两次,这会带来一些其他风险。

因此,rust不允许这种现象存在(为什么在js中可以,因为js的内存回收机制不一样,js中,只有有引用,内存就不会回收。)。

了解了所有权规则2之后,规则3就相对简单了。我们接着上代码:

fn main() {
    let a = 4;
    {
        let b = 5;
    }
    a + b;
}
复制代码

上面这行代码运行很显然会报错,因为很明显,我们在花括号以外的地方引用了b,这是已经不在b的作用范围内了,也就是违反了规则3,所以肯定会报错。

总结

rust中的所有权规则最难理解的就是第二点。如果需要完全理解,需要你对cs的基础知识尤其是堆、栈部分有一些基本的了解。但是如果一时不理解也没关系,只需要记住,rust中的复杂类型一旦被赋值给了另外一个变量,那么之前的变量就会立即失效了。所以,不要在rust中把轻易把复杂类型的值赋给另外一个变量。

如果确实需要的话怎么办呢?这就是我们后面要讲的内容了。


如果你对rust中的所有权规则不理解的话,你肯定无法愉快的使用rust,因为这些规则会让你感到很困惑,你会觉得rust的代码是那么的不合逻辑。但是等你理解了之后,我相信你会爱上这个伟大的设计。就像香菜一样,开始的时候觉得臭,但是一旦你爱上,就会日思夜想;又或者就像那个你曾经看不上的人,等你回过神来的时候却发现早已是你的心头挚爱。

就这样了各位,码字不易,给个赞吧~😭

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值