Rust-Lang Book Ch.4 Ownership

Ownership

Ownership使得Rust能够无需额外的garbage collector线程就确保内存安全。在编译时,Rust就通过一系列规则并确定Ownership。Ownership与Borrowing, slices和Rust在内存中如何排列数据有关。

在许多编程语言中,数据在stack上还是在heap上不会对性能有很大影响。但是在Rust中则不然。存在stack中的数据必须已知具体所占空间大小,否则就应该放到heap上。Stack与Heap在性能上的不同主要有如下两点:1. 推送数据到Stack上要比在heap分配空间要更快,这是因为allocator不需要去寻找能够容纳当前数据的地址。2. 在stack中获取数据要比在heap中更快。这是因为heap可能要通过不紧邻的指针来取值

Ownership Rules

Rust中的每个值都有一个管理其生命周期的变量称为Owner。同一时间只能有一个Owner。当Owner出了作用域后,该值就从内存中抹去。

Variable Scope

    {                      // 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为例阐释Rust的OwnerShip机制

string literal所占空间已知,同时数值已知,但是是immutable的。可改的String变量必须要从string literal转化而来。显而易见的,string literal的具体内容编译的时候就已知了,所以Rust就直接将literal放入到最终的目标格式文件中,因此,string literal本身能够保持快速+高效。不过,这也导致了这些literal都是immutable的。

而在String这个类型中,则能够支持可变的text,编译器会在heap上分配所需空间,而这些在编译的时候是未知的。为此,这些memory需要被memory allocator在运行的时候请求,并且当这个String变量出了作用域之后被释放掉。具体地,Rust会在Variable出了作用域之后调用一个名为drop的函数。drop函数的基本模式与c++的RAII(Resource Acquisition Is Initialization,资源获取即是初始化)十分相似。

String具体由三部分构成: 1. 一个指向内容所在内存的指针 2. 内容的长度 3. 目前的容量capacity。对于`let s2 = s1`这样一句话,不会发生内容的拷贝,只会发生指针的拷贝。在指针拷贝后,这片内存就有多个值同时使用,当出了作用域之后,也就会引发两次free。如何避免double free呢?1. 在s2=s1这句之后,编译器会标记s1是个无效变量,如果这之后再去用s1,Rust会直接报错:borrow of moved value: `s1`。2. s2=s1时发生了Move(类似Shadow Copy)

    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}, world!", s1);
//^^ value borrowed here after move

如果希望Deep Copy这个String,那么我们应该调用clone方法。要注意clone方法相对于move更耗时耗空间。

String类型的变量s1是空间未定且存储在heap上的,而对于空间大小已经由类型给定,且存储在stack上的变量,Copy是可以轻松进行的,也即此时Move(shadow copy)和Clone(deep copy)本质上是相同的。例如let i1=5;let i2 = i1;就不需要invalidate i1。Rust有一个特别的Annotation名为Copy,可以让类似Integer的类型自动拷贝赋值,而且不会使得赋值后旧的变量变得无效。注意如果要实现Copy的类型不能有任何成员实现了Drop特性,如果实现了Drop特性,也即要求该类型在出作用域时做出什么特定行为,就会得到编译错误。注意Tuples如果成员都是可以copy的,那么Tuple本身也可以Copy。

Ownership and Functions

传参这一行为和赋值也非常相似,也就是说,将一个变量传入、传出函数就会发生move或者copy这种行为。如果将s1: String传入一个函数,并且在这之后仍然要使用s1,编译器就会报错。如果一个变量包含在heap上的数据出了作用域,那么在heap上的数据就会被清理掉,除非这个变量之前被moved,并且将ownership转交给了另外一个变量。

那么如果我们希望一个函数使用一个变量,但是不要发生ownership的转移呢?传入之后再把所有变量返回并且赋值实在是太麻烦了。这时,我们可以使用reference,即引用,符号&。

References and Borrowing

像c语言一样,Reference使用符号&,而Dereference使用符号*。引用本身相当于一个指向变量的指针,使得变量本身的控制权不会发生改变。Rust称使用references来作为函数参数的行为为borrowing,即借用。

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, nothing happens.

  

    let s1 = String::from("hello");

    let len = calculate_length(&s1);

  

要区分&String与c++中的reference。c++中的reference是能够修改变量内容的,而Rust中的&String则仅是不会发生所有权的Move。如果想要修改变量内容,依然要加上mut这个关键字,使之成为可修改的引用,即必须传给函数&mut String类型的变量,函数才能修改String而且不发生变量所有权转让。注意mutable reference有个限制,那就是在一个作用域中,一次只能存在一个有效的mutable reference。(难道mutable reference是单例的?)

let r1 = &mut s;
         ------ first mutable borrow occurs here
let r2 = &mut s;
         ^^^^^^ second mutable borrow occurs here
println!("{}, {}", r1, r2);
                  -- first borrow later used here

  

此外,immutable reference和mutable reference也存在如下限制:要么同时多个用于读的immutable reference,要么单个mutable reference。

    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);

这样限制的目的是为了避免data race。

Data race发生在如下场景:

1. 多个指针同时使用同块数据。

2. 至少其中一个执行写操作

3. 程序员没有附加该如何处理这种读写冲突或者写写冲突的说明

这种data race可能导致非常隐秘的bug,所以Rust直接规定只准有一个mutable reference,只准有一个指针写这块数据,从根本上避免冲突。

不过,reference用过了不再使用就可以声明新的,并不是要一直出了作用域才能声明新的。

    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // r1 and r2 are no longer used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);

  

在Rust中,编译器保证不会产生Dangling Reference,也即在数据出了作用域之后,reference本身也就不能再使用,否则编译器报错。

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
                     ^ help: consider giving it a 'static lifetime: `&'static`
    let s = String::from("hello");

    &s
}

  

Slice类型

Slice在传入传出的时候是无需转换Ownership的。Slice使得你能够使用一个集合中的部分元素,而不需要直接传入整个集合。

String的slice的类型是&str。

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!
//^^^^^^^^^ mutable borrow occurs here

    println!("the first word is: {}", word);
//                                             ---- immutable borrow later used here

}

  

编译器会保证引用类型-也即这里的tuple,在作用期间保持有效。如果将String s的slice赋给word这个变量,那么在word使用完毕结束生命周期之前,s本身就不能再改动。

这里要说明的是,对于let s = "Hello world!"这样一句赋值,s也是一个slice指向内存中的这块string literal。所以s的类型就是&str。

此外,在只读的函数first_word中,与其传入&String,不如传入&str,这样就能够在string slice(和string literal)上进行操作,而String转为string slice不耗费什么时间空间,string slice转化为String却是相反的。

//fn first_word(s: &String) -> &str {
//改为
  fn first_word(s: &str) -> &str {

  

    let word = first_word(&my_string_literal[..]);

  

其他类型的slice,作为array的一部分,通常类型名称为&[T],例如&[i32]。所有slice的操作和原理都一样,存储第一个元素和其总长。

原文地址:https://www.cnblogs.com/xuesu/p/13861015.html