Rust基础复习笔记(一):所有权
所有权是Rust的核心内容,需要清楚地了解它的意义和机制。这里参考The Rust Book的第四章,详解Rust的所有权机制。本章分为三个部分,所有权、引用与切片。
所有权
首先先看一下String字符串类型
let s1 = "Hello!";      //字符串字面量
let mut s2 = String::from("Hello!");      //String类型
上者是硬编码,下者则是可变的,可以用来记录输入的内容。这两者的区别来自于不同的内存分配机制。后者会在堆中申请内存,并在运行时发生变化。这里便涉及到了所有权的核心机制:
当拥有这块内存的变量离开作用范围时,其自动被释放,这个过程被称为drop:
fn main() {
    {
        let s = String::from("hello"); // s is valid from this point forward
        // do stuff with s
    }                                  // this scope is now over, and s is no - 被释放了
                                       // longer valid
}
所有权在赋值过程中移交,但是大小已知的基本类型则会执行复制操作,并不会导致赋值者失效:
      let x = 5;
      let y = x;             //这里y=x=5
      let s1 = String::from("Hello!");
      let s2 = s1;           //这里s1不再指向字符串
      //println!("{}",s1);   //这句会报错
因此与其它语言中的“深复制(deep copy)”和“浅复制(shallow copy)”等不同,这里用的说法是“移动(move)”。
不过你也可以显式指定深复制方法,这种情况下不会取消掉前面的变量:
      let s1 = String::from("Hello!");
      let s2 = s1.clone();      //both valid
扩展:之前提到有固定大小的变量只是进行复制,但实际上是因为它们拥有一个名为
Copy的属性(trait),这个属性使赋值者一直保持有效。当然,这个属性和另外一个对立的属性:Drop不能共存。需要指出的是,如果一个元组里所有元素都有Copy属性,这个元组视为有这个属性。但凡出现了一个没有的,这个元组都不会保持这一特点。关于属性的具体特点在另一篇系列博文里有记录。
在作为参数传递时,所有权也会被移交。有Copy属性者不受此影响:
      let s = String::from("Hello!");
      takes_ownership(s);      //s这之后就没了,随着这个函数的结束而释放掉。
作为返回值传递时,也会发生所有权的移交
      let s1 = gives_ownership();
      //如果gives_ownership()是一个返回字符串的函数,该返回值的所有权就移交到了s1手里
引用与借用
很多时候函数把变量直接取走会增添很多麻烦,所以可以传递引用来避免这件事发生:
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);      //这里要标注引用啊!
    println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
    s.len()
}
函数中的s就是一个引用。引用本质上来讲就是指针,而不是一个浅复制或其他类似的东西。

有了引用自然就有解引用。解引用的符号也是'*',在后面会讲到。
尽管如此,上面的引用仅仅借走了值,不能对它进行修改。修改的情况需要添加可变引用(&mut)。
fn main() {
    let mut s = String::from("hello");
    change(&mut s);                        //这里是加在变量名之前
}
fn change(some_string: &mut String) {      //这里是加在类型名之前
    some_string.push_str(", world");      
}
不过可变引用的使用十分严格。它对于一个变量(在相同的作用域中—— 这里scope为{}分隔)不仅只能同时存在一个,还不能在其他不可变引用的作用域里声明使用(这里的作用域指不可变引用从声明到最后一次使用之间的范围)。
下面举出几个正确/错误的代码示范。代码来自文档。
fn wrong_one(){
    let mut s = String::from("Hello!");
    let r1 = &mut s;
    let r2 = &mut s;      
    println!("{},{}",r1,r2);
    //只能存在一个
}
fn wrong_two(){
    let mut s = String::from("Hello!");
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;
    println("{}{}{}",r1,r2,r3);
    //不能出现在不可变引用作用域里
}
fn right_one(){
    let mut s = String::from("hello");
    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.
    let r2 = &mut s;
}
fn right_two(){
    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不会让垂悬引用通过编译。
fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");
    &s      //在这里返回引用的话,引用指向的内容会在结束后被释放。因此这段代码不会通过。
}
这个时候直接返回变量就好了。
最后文档给出了使用引用的基本原则:
- 只使用一个可变引用或多个不可变引用
- 保证引用指向的内容有效(即不出现垂悬引用)
切片(Slice)
切片是一种比较特殊的引用。
文档里用了寻找字符串中第一个单词的例子来引入。首先给出了几个程序代码使其返回单词的起止索引值。不过我们对这个不感兴趣,只要了解它想说的内容即可:
- 传统的做法会导致生成多个和原字符串互相独立的变量。即字符串消失了,这些变量仍然有效地存在,即使已经失去了它们本来的含义。这一点是易于致错(error prone)的!
因此我们采用了切片的方法:
    let s = String::from("Hello World");
    
    let hello = &s[..5];      //等价于 &s[0..5]
    let world = &s[6..11];
除了省略0,还有其他特殊的写法。包括采用变量&s[0..a]或者干脆全部省略&s[..]。
切片有自己的特殊类型:&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!
    println!("the first word is: {}", word);
}
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 | 
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 | 
20 |     println!("the first word is: {}", word);
   |                                       ---- immutable borrow later used here
error: aborting due to previous error
...
注意这里是由于出现了可变引用。clear()方法自然要使用s的可变引用,而切片已经是不可变引用了。因此巧妙避免了错误的发生。
这里的切片只针对ASCII码,对于UTF-8码和其他情况会有后续讨论
这里我们还可以深入理解一下字符串字面值。它们本质上也是&str类型,也就是说,本质上是个不可变引用值。
let s = "Hello, world!";
还有一个小技巧:当要传递字符串作为参数时,不妨考虑使用&str而非&String。因为前者既能传递字面值和切片,也能传递String变量(全切片形式),泛用性更广:
fn main() {
    let my_string = String::from("hello world");
    // 传递全切片
    let word = first_word(&my_string[..]);
    let my_string_literal = "hello world";
    // 传递字面值的切片
    let word = first_word(&my_string_literal[..]);
    // 传递字面值,你甚至不用加'&'!
    let word = first_word(my_string_literal);
}
最后了解一下其他切片:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];      //同样是存储头索引和长度。拥有数据类型&[i32]
在介绍动态数组(vector)的时候会更详细介绍这一部分内容。
以上就是Rust所有权的核心内容。但这并不是全部内容。所有权思想涉及Rust编程的方方面面,将在以后进行更深入的讨论。
另外由于是第二次阅读,感觉理解程度要远高于第一次阅读。记忆也会更加深刻吧。
 
                    
                
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号