Rust学习五 所有权

rust的核心特性所有权

Posted by rpl on April 27, 2020

所有权是rust最核心的特性。所有的编程语言都必须管理程序在运行时使用的内存,有的程序使用垃圾回收策略释放内存,有的程序则必须手动的分配和释放内存,而rust选择了第三种方式:内存是通过所有权系统和一组规则来管理的,编译器在编译时检查这些规则。当程序运行时,没有任何所有权特性会减慢程序的运行速度。

通过此链接查看rust学习系列的其他文章

一、堆与栈

堆和栈都是内存的一部分, 栈按照获取值的顺序储存值,按照相反的顺序删除值,后进先出。将栈想象成盘子,每次都是将最新的盘子放在最上方,取盘子的时候也都是取最上方的。将数据入栈、出栈称为 pushpop 。 因为栈无论存取都只操作顶端的元素,所以访问速度很快。此外由于所有栈数据都是大小已知、长度固定的,这也能加速栈的访问速度。

而大小未知,或者长度可变的数据都无法存储在栈上,这类数据就只能放在堆上。堆得组织性较差:我们要将数据放到堆上时,必须申请一块空间,操作系统会分配一块大小合适的内存,并将这块内存的地址指针返回给我们。这一过程称为 allocating 。由于指针是大小已知,长度固定的数据, 它可以存储在栈上。 之后我们就可以通过这个指针找到这块内存的数据了。

访问堆数据要比栈元素慢很多,因为要先访问在栈上的指针,在通过指针才能找到堆数据。设想一下,在一家餐馆里,一个服务员从许多桌子上点菜。在移到另一张桌子之前,将当前桌子的菜单全部获取是最有效的。从桌子A取一个菜单,然后从桌子B取一个菜单,然后再从A取一个菜单,然后再从B取一个菜单,这将是一个慢得多的过程。同样,如果处理器处理离其他数据较近的数据(因为它在栈上),而不是离得较远的数据(因为它在堆上),处理器可以更好地完成工作。此外在堆上分配大量空间也需要时间。

当代码调用一个函数时,传递到函数中的值(可能包括指向堆上数据的指针)和函数的局部变量将被压入栈。当函数结束时,这些值将从栈中弹出。跟踪代码的哪些部分使用了堆上的哪些数据,最小化堆上的重复数据量,以及清理堆上未使用的数据,以免耗尽空间,这些都是所有权所解决的问题。一旦理解了所有权,就不需要经常考虑堆栈和堆,但知道管理堆数据是所有权存在的原因 ,可以帮助解释为什么它是这样工作的。

二、所有权

所有权规则

  1. Rust中的每个值都有一个对应的变量,叫做它的所有者。
  2. 在某一个时刻一个值只能有一个所有者。
  3. 当所有者超出作用域时,它的值会被销毁。

变量作用域

rust的作用域与变量有效性之间的关系与其他编程语言是相似的:当变量进入作用域时,它是有效的,直到变量离开作用域。

1
2
3
4
5
{                     // s is not valid here; it's not yet declared
    let s = "hello";  // s is valid from this point forward
    
    // do stuff with s
}                     // this scope is now over, and s is no longer valid

String类型

1
let s = String::from("Hello");

双引号:: 是一个操作符,它允许我们去命名空间这个在String下的特定的from 函数,而不是使用其他的命名方式, 如 String_from。

String类型是可变的,而字符串是不可变的:

1
2
3
4
5
let mut s = String::from("Hello");
s.push_str(", world"); 

let s2 = "hello"; // 不可变
println!("{}", s);  // this will print 'hello, world'

因为String类型的数据是大小未知,长度可变的,它是存储在堆上的,所以本章使用String类型的数据作为示例来学习rust所有权。

内存与分配

两种数据类型的不同,是由它们在内存中的不同的存储方式决定的。字符串是不可变的,在编译时,字符串类型的数据对于编译器来说是大小可知,长度固定的,它会存储在栈上,所以字符串的访问速度是非常快的。但是字符串是不可变的,在程序运行中如果想要改变文本字段,我们就要使用String类型了。String类型由此会存储在堆上。

数据存储在堆上,这就意味着:

  1. 程序必须在运行时向操作系统申请内存分配。
  2. 我们需要在使用完String数据后释放内存。

第一步我们在调用代码 String::from 的时候,会实现需要请求的内存。绝大多数编程语言的实现方式都是类似的。

但是在第二步内存回收中体现了不同语言之间的差异:

部分语言例如 Python会使用垃圾回收系统(garbage collector GC)。 CG会保持追踪和清除不在使用的数据,从而释放内存。

没有GC的话,如C/C++就需要我们去识别哪些数据不在使用, 在代码里显式的,手动释放内存。这点是很难完全正确的做到的。如果我们忘记释放某个变量,会导致内存浪费。提前释放又会导致变量失效。多次释放则会出现bug。我们很难精确的匹配每一次的分配和释放。

相比于前两种,Rust是一旦一个变量超出了它的作用域,rust会自动调用drop函数回收内存。 drop函数在右花括号处会被自动调用。

三、所有权对变量赋值的影响

Move

1
2
3
4
5
let x = 5;
let y = x;  // x still vaild

let s1 = String::from("Hello");
let s2 = s1;  // s1 no longer be vaild
  1. s2 = s1 的操作, s2只复制s1在栈内的指针信息,从而指向堆内的数据。
  2. rust的所有权系统会使s1失效,从而不会出现s1,s2都指向同一个堆数据的情况,因为这会导致双重释放。
  3. s2 = s1 只复制了栈信息,没有复制堆信息,很像如Python语言的浅复制,但是s1同时也失效了,我们将 s2 = s1称为移动(move) 。 数据的所有权从s1移动到了s2。 这也更好的解释s1的失效。

Clone

有时候我们想要的就是完完全全的复制一个变量, 类似于深拷贝,在rust中称为克隆(clone)

1
2
3
4
s1 = String::from("Hello");

s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

clone()会复制栈和堆上的数据。当看到对clone的调用时,就知道正在执行一些专制的代码,而且这些代码的开销可能很昂贵。这是一种视觉指示,表明某些不同的事情正在发生。

Copy

1
2
3
4
let x = 5;
let y = x;  // x still vaild

println!("x = {}, y = {}", x, y)
  1. 因为x,y都是编译时大小已知的整数类型,完全存储在堆栈中,因此可以快速复制实际值
  2. 这意味着我们没有理由在创建变量y后阻止x的有效性。
  3. 换句话说,这里的深复制和浅复制没有区别,所以调用clone与通常的浅复制没有什么不同,我们可以把它省去

rust中可copy的数据类型:

  1. 所有的整型数据
  2. 布尔数据: true, false
  3. 字符串类型的数据。
  4. 浮点型数据。
  5. 元组类型, 但仅仅是元组内的元素都可复制时才可以。

四、所有权与函数

函数中传参与所有权的转移

传递一个值给函数与给一个变量赋值是相似的。

给函数传参将会进行move或copy的操作,move之后变量就会失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {     
    let s = String::from("hello");  // s comes into scope     takes_ownership(s);             
                                    // s's value moves into the function...
                                    // ... and so is no longer valid here     
    let x = 5;                      // x comes into scope     
    makes_copy(x);                  // x would move into the function,                                     
                                    // but i32 is Copy, so it's okay to
                                    // still use x afterward 
} // Here, x goes out of scope, then s. But because s's value was moved,   
  // nothing special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope     
    println!("{}", some_string); 
} // Here, some_string goes out of scope and `dropìs called. The backing   
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope     
    println!("{}", some_integer); 
} // Here, some_integer goes out of scope. Nothing special happens.

函数返回值与所有权的转移

返回值也会转移所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
    let s1 = gives_ownership();  // gives_ownership moves its return value into s1
    
    let s2 = String::from("Hello");  // s2 comes into scope
    
    let s3 = takes_and_gives_back(s2);  // s2 is moved into takes_and_gives_back, which alse
                                        // moves its return value into s3
    
} // here, s3 goes out of scope and is dropped.

fn gives_ownership() -> String {
    let some_string = String::from("hello");
    some_string    // return and moves out to the calling function
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string
}

变量的所有权每次都会遵循以下原则:

  1. 赋值给另一个变量会转移所有权
  2. 当一个含有堆数据的变量超出作用域时,值会被drop函数清除。除非数据的所有权已经转移给了另一个变量

此外函数可以通过元组返回多个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
    let s1 = String::from("hello");
    
    let (s2, len) = calculate_length(s1);
    
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String 
    
    (s, length)
}

五 words, statements and translation

  1. memory is managed through a system of ownership with a set of rules that the compiler checks at compile time. None of the owner- ship features slow down your program while it’s running.

    内存是通过所有权系统和一组规则来管理的,编译器在编译时检查这些规则。当程序运行时,没有任何所有权特性会减慢程序的运行速度。

  2. Now we’ll build on top of this understanding by introducing the String type.

    现在,我们将在这种理解的基础上引入字符串类型。

    build on top of … 建立在…之上

  3. This pattern has a profound impact on the way Rust code is written.

    这种模式对Rust代码的书写方式有深远的影响。

    profound 深刻的,深厚的, 意义深远的

    impact 影响

  4. If Rust did this, the operation s2 = s1 could be very expensive in terms of runtime perfor- mance if the data on the heap were large.

    如果Rust做到了这一点,那么如果堆上的数据很大,则操作s2 = s1在运行时性能方面可能会非常昂贵。

    in terms of 就…而言, 在…方面, 按照,依据

  5. security vulnerabilities 安全漏洞

    vulnerability 缺陷, 弱点, 漏洞

  6. There’s another wrinkle we haven’t talked about yet

    还有一个问题我们还没有讨论

    wrinkle 皱纹

  7. contradict 矛盾

  8. It’s quite annoying that anything we pass in also needs to be passed back if we want to use it again, in addition to any data resulting from the body of the function that we might want to return as well.

    这很烦人,除了我们想要返回的函数体所产生的任何数据,我们传入的任何数据也需要传回来如果我们想再次使用它。

    in addition to 除了…之外(还有, 也)