Rust中所有权与借用规则概述

139次阅读
没有评论

共计 3960 个字符,预计需要花费 10 分钟才能阅读完成。

内容目录

 所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。目前主要有以下三种流派:

  • 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表就是:Java、Go;
  • 手动管理内存的分配和释放,在程序中,通过函数调用的方式来申请和释放内存,典型代表:C/C++
  • 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查,Rust就是使用这种方式。

1. Stack And Heap

Stack-栈

 栈按照先进后出的顺序存取值。基于这种实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,就无法从栈中得到我们想要的数据

Heap-堆

 对于大小未知或者可能变化的数据,一般是存储在堆上的。每当向堆中放入数据时,就需要请求一定大小的内存空间,操作系统会在堆中的某处找到一块足够大的空间将其标记为已使用,并返回一个表示该位置地址的指针,整个过程称为在堆上分配内存

 当分配完成后,该空间的指针就会入栈,因为指针的大小是已知且固定的,所以在后续使用的过程中,会通过栈中的指针来获取在堆中的实际内存位置,从而访问到数据

性能区别:一般情况下,在栈上分配内存要比在堆上分配内存快得多。因为入栈无需进行函数调用,只需将数据放到栈顶即可,而在堆上分配内存,则需要让操作系统先找到一块足够的内存空间,然后进行记录为下一次分配做准备,如果当前内存页不足,还需要进行系统调用申请更多的内存。

 当代码调用一个函数时,传递给函数的参数依次被压入栈中,当函数调用结束时,这些值将根据栈的规则弹出。因为堆上的数据缺乏组织,所以跟踪这些数据何时分配和释放就显得非常重要,否则堆上的数据将产生内存泄漏(数据无法回收)。

2. 所有权原则

  • Rust中每一个值都被一个变量所拥有,该变量称为该值的所有者
  • 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  • 当所有者(也就是变量)离开作用域范围时,这个值将会被丢弃

例如下述

{                      // s 在这里无效,它尚未声明
    let s = "hello";   // 从此处起,s 是有效的

    // 使用 s
}                      // 此作用域已结束,s不再有效

2.1 变量绑定背后的数据交换

转移所有权


{ // 作用域开始
    let x = 5;
    let y = x;
} // 作用域结束

上述代码中所有权并没有发生转移,也就是说:x拥有5,y也拥有5。原因如下:

  • 在Rust中,基本数据类型是固定大小的简单值,对于这种简单值,在赋值时是通过自动拷贝的方式赋值的;
  • 这种自动拷贝的方式赋的值都被存在栈中,无需在堆上分配内存;
  • 由于赋值过程发生在栈中,因此并不需要所有权转移;

{ // 作用域开始
    let s1 = String::from("hello");
    let s2 = s1;
} // 作用域结束

 上述代码中s1的所有权发生了转移,将其转移给了s2,原因如下:

  • String不是基本数据类型,其内存分配发生在堆中,当在堆中找到合适大小的空间后,会返回一个堆指针,存储在栈中的数据就包括:堆指针、字符串长度、字符串容量。其中字符串长度是实际字符串的长度大小,容量是堆分配的空间大小;
  • 如果s1和s2都指向同一位置,那么当这两个变量离开作用域后,都会对同一位置进行清理,这就会造成二次释放,二次释放同一内存空间会导致内存污染,甚至导致一些隐藏的安全漏洞
  • Rust为了避免二次释放解决上面的问题,就会进行所有权转移:当s1被赋予给s2后,Rust认为s1不再有效,因此无需等待s1离开作用域才清理内存空间,而是把数据的所有权从s1手上转移给了s2,之后s1不再有效.
  • 这样,s1因为是无效的,s2是有效的,当s1,s2离开作用域时,就只有s2会释放内存。

 当我们在s1转移所有权后再次使用s1,那么就会报错:

Rust中所有权与借用规则概述

它提示我们使用了一个移动(move)的值。

 对于了解深拷贝和浅拷贝概念的人来说,应该很容易想到使用深拷贝实现s1和s2都拥有字面量为"xxx"的数据。在Rust中,所有自动复制的行为都是基于浅拷贝实现的,深拷贝都需要我们手动实现。在上面的代码中,s2=s1就是一个浅拷贝行为,这就导致s1和s2都指向同一个堆内存,因此发生所有权转移。

Rust中所有权与借用规则概述

 当我们使用深拷贝时,就可以避免所有权转移。在String类型中,可以使用clone()方法得到一个深拷贝的数据。

fn main() {
    let s1 = String::from("Hello World");
    let s2 = s1.clone();
    print!("{}", s1);
}

 深拷贝不再仅仅只是将栈上的数据拷贝一份,还会把堆内存中的数据拷贝一份。clone方法会有一定的性能损耗,在其它语言中基本也是这样。这也就是为什么Rust中所有的拷贝行为都是基于浅拷贝而不是深拷贝。

Rust中所有权与借用规则概述

当然,我们还可以对字符串的引用进行浅拷贝:

fn main() {
    let s1: &str = "Hello Wolrd";
    let _s2 = s1;
    print!("{}", s1);
}

 在上面的代码中,s1只是引用了存储在二进制中的字符串"Hello Wolrd",并没有持有所有权。当执行s2=s1时,也只是将引用进行了拷贝,s1和s2都引用了同一个字符串。

3. 函数传值与返回

fn main() {
    let s1 = String::from("Hello Wolrd");
    move_value(s1);
    println!("s1:{}", s1);
}

fn move_value(str: String) {
    let s2 = str;
    println!("s2:{}", s2);
}

 上述代码在执行时会报错,提示我们使用了一个move值。根据所有权原则,不难发现s1在调用move_value后s1的所有权转移给了s2,s1失效,move_value执行完毕后,s2离开作用域被丢弃。也就是说,我们的s1在传入move_value函数,并且执行完毕后,s1的值失效了。为了解决这个问题,我们可以选择函数返回值将原来的值返回出来。代码如下:

fn main() {
    let s1 = String::from("Hello Wolrd");
    let s1 = move_value(s1);
    println!("s1:{}", s1);
}

fn move_value(str: String) -> String {
    let s2 = str;
    println!("s2:{}", s2);
    s2
}

 Rust中所有权机制非常强大,避免了内存的不安全性,但是也带来了一个新麻烦: 总是把一个值传来传去来使用它。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题,那就是借用。

4. 借用

获取变量的引用就是借用。Rust正是通过借用避免因为所有权机制造成值被传来传去。

1. 引用

 常规引用就是一个指针类型,指向了对象存储的内存地址。和C/C++中的语法很像,可以是&去取地址,*去获取指针所执行的数据。

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

基于此,我们可以使用下述代码来简化move_value函数:

fn main() {
    let s1 = String::from("Hello Wolrd");
    move_value(&s1);
    println!("s1:{}", s1);
}

fn move_value(str: &String) {
    let s2 = str;
    println!("s2:{}", *s2);
}

 但是,正如变量默认不可变的一样,引用指向的变量也是默认不可变的,因此当我在上述代码中尝试对s2指向的数据进行修改,就会报错:

Rust中所有权与借用规则概述

 因此要想让引用的变量数据可变,也需要使用mut关键字:

fn main() {
    let mut s1 = String::from("Hello Wolrd");
    move_value(&mut s1);
    println!("s1:{}", s1); // s1:Hello Wolrd!
}

fn move_value(str: &mut String) {
    let s2 = str;
    s2.push_str("!");
    println!("s2:{}", *s2); // s2:Hello Wolrd!
}

 需要注意的是,Rust中一个作用域中只能存在一个对同一数据的可变引用。当出现多个对同一数据的可变引用是,编译器就会报错。代码如下:

fn main() {
    let mut s = String::from("Hello Wolrd");
    let s1 = &mut s;
    let s2 = &mut s;
    println!("s1:{},s2:{}", s1, s2)
}

Rust中所有权与借用规则概述

这一限制的好处就是可以让Rust在编译期就避免数据竞争,数据竞争可由以下行为造成:

  • 两个或两个以上的指针同时访问同一数据;
  • 至少有一个指针被用于写入数据;
  • 没有同步数据访问的机制;

数据竞争会导致未定义行为,这种行为很可能超出我们的预期,难以在运行时追踪,并且难以诊断和修复。而 Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!

 除此之外,还有一个需要注意的点就是:可变引用与不可变引用不同同时存在

fn main() {
    let s = String::from("Hello Wolrd");
    let s1 = s;
    let s2 = &mut s;
    println!("s1:{},s2:{}", s1, s2)
}

 这点也好理解,毕竟s默认是不可变的,怎么会允许你通过引用去让它改变呢。

总结

  • 所有权原则
    • 每一个值都被一个变量拥有,该变量称为该值的所有者;
    • 一个值同时只能被一个变量所拥有,一个值只能拥有一个所有者;
    • 所有者离开作用域时,该值将会被丢弃
  • 借用规则:
    • 同一时刻,你要么只能拥有一个可变引用,要么拥有任意多个不可变引用;
    • 引用必须总是有效的;
正文完
 
PG Thinker
版权声明:本站原创文章,由 PG Thinker 2024-10-01发表,共计3960字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
热评文章