Rust所有权进阶部分

为了演示所有权功能,我们需要一些复杂的数据类型,之前介绍的类型都是存储在栈上的并且当离开作用域就被移除栈,不过我们需要一个存储在堆上的数据来探索Rust是如何知道该在何时清理数据的。

这里使用String作为例子,它的一些例子可能也适用于标准库的或者你自定义的一些复杂数据类型,Rust中有两种字符串类型,第一种是直接使用 let s ="hello",这一种很明显不能满足要求,因为他们是不可变的,但是往往有时候我们想创建一个可变的变量。比如用户要输入时; let s = String::from("hello"), 这样就可以修改这个变量了。

fn main() {
    let mut s = String::from("hello");
    s.push_str(" wolrd");

    println!("{}", s);
}

内存与分配

 就字符串字面值来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件终。这使得字符串字面值快速且高效。不过这些特性都值得益于字符串字面值的不可变性,不幸的是,我们不能为了每一个在编译时大小未知的文本而将一块内存放进二进制文件中

,并且他的大小还可能随着程序运行变化。

对于String类型,为了支持可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存存放内容,这意味着:

1、必须在运行时向操作系统请求内存;(调用String::from时,它的实现(implemetation)请求其所需的内存)

2、需要一个当我们处理完String时将内存返回给操作系统的方法,(在其他有GC的语言中,会自动清除不再使用的内存,在没有GC的语言中,就需要程序员自己allocate配对一个free)

Rust中采取了一个不同的策略:内存在拥有它的变量离开区域时就被自动释放。如上面的例子,当main函数的s变量在离开了{}作用域后,会触发一个drop方法清除内存。

变量与数据交互的方式(一):移动

1、基础数据/不可变数据存放在栈中,通过赋值拷贝创建另一个变量

fn main() {
    let x = 5;
    let y = x;

    println!("x:{}", x); // 不可变数据存放在栈上,进行拷贝吗,此时栈上有两个5

    let s1 = String::from("hello");  
    // 首先在栈上创建一组数据:指向字符串的内容指针+长度+容量
    // 然后在堆上存放字符数据
    let s2 = s1;
    // 当s2指向s1时,会在栈上拷贝份s1给s2,但是这意味着栈上有两个数据指向同一个内存
    // 如果要清除就会发生“二次释放(double free)”的错误,为了解决这个问题,Rust在
    // 将s1赋值给s2后,s1就失效了。
    // println!("s1:{}",s1);
}

  在其他语言中,可能叫“浅拷贝”,在Rust中同时使第一个变量无效了,这个操作称为移动。

变量和数据交互方式(二):克隆

如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone的通用函数。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1:{},s2:{}", s1, s2);
}

效果是在栈上创建两个变量,指向两个堆上的数据。

只在栈上的数据:拷贝

fn main() {
    let x = 5;
    let y = x;

    println!("x:{},y:{}", x, y); // 不可变数据存放在栈上,进行拷贝吗,此时栈上有两个5
}

可以看到上段代码并没有使用clone,不过x依旧没有失效被移动到y上,原因是像整型这种的在编译时就已知大小的类型被整个存储在栈上,所以拷贝其实实际的值是快速的,这意味着没有理由再创建变量y后x无效,且Rust有一个叫做Copy trait的特殊注解,可以用在类型类似整型这样的存储在栈上的变量,如果一个类型拥有copy trait的类型,一个旧的变量在讲其变量赋值后依旧可用。满足这种功能的类型有:

1、所有的整型
2、布尔类型
3、所有浮点型
4、字符类型
5、元组,当且仅当包含的类型也是满足Copy类型的

所有权与函数

将值传递给函数在语义上与给变量赋值相似,向函数传递值可能会移动或者复制,就像赋值语句一样。

fn main() {
    let s = String::from("hello"); // s 进入作用域
    takes_ownership(s); // s 的值移动到函数里 ...
                        // ... 所以到这里不再有效
    let x = 5; // x 进入作用域
    makes_copy(x); // x 应该移动函数里,
                   // 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作
fn takes_ownership(some_string: String) {
    // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) {
    // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

返回值与作用域

返回值也可以转移所有权。

fn main() {
    let s1 = gives_ownership(); // gives_ownership 将返回值移交s1

    let s2 =String::from("hello"); // s2进入作用域
    let s3 = take_and_gives_back(s2); // s2被移动到函数中,它将返回s3
}  // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,所以什么也不会发生,s1被移出作用域并丢弃

fn gives_ownership() -> String{
    let some_string = String::from("hello");
    some_string
}

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


转移返回值的所有权

变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它,当持有堆中数据值的变量离开作用域时,其值将通过drop被清理掉,除非数据被移动为另一个变量。如果一个变量我们传给函数,但是还想拿到所有权,并且获得其他数据,这时候就可以使用元组作为返回值。

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() 返回字符串的长度 
    (s, length) 
}

返回参数的所有权

使用引用(reference),上面的例子中,为了main函数中还能使用s1,我们必须将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()
}

这些 & 符号就是 引用,它们允许你使用值但不获取其所有权,我们将获取引用作为函数参数称为 借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。

尝试修改借用的值,剧透这是不行的

fn main() { 
    let s = String::from("hello"); 
    change(&s); 
}

fn change(some_string: &String) { 
    some_string.push_str(", world"); 
}
// 正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值

可变引用

 我们通过一个小调整就能修复上面不可以修改引用值的问题。

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

// 必须将 s 改为 mut 。然后必须创建一个可变引用 &mut s 和接受一个可变引用 some_string: &mut String 。

不过可变引用有一个很大的约束,在特定作用域中的特定数据有且只有一个可变引用

fn main() {
    let mut s = String::from("hello"); 
    let r1 = &mut s; 
    let r2 = &mut s; 
    println!("{}, {}", r1, r2);
}

//错误
// let r1 = &mut s; | ------ first mutable borrow occurs here 
// let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here

这个限制允许可变性,不过是以一种限制的方式允许,这个限制的好处是Rust可以在编译时就避免数据竞争(data race)。

数据竞争的造成原因:
    1、两个或更多指针同时访问同一个数据
    2、至少有一个指针被用来写数据
    3、没有同步数据访问的机制

一如既往,可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同 时 拥有:

let mut s = String::from("hello"); 
{ 
    let r1 = &mut s; 
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用 

let r2 = &mut s;

类似的规则也存在于同时使用可变与不可变引用中。这些代码会导致一个错误:
let mut s = String::from("hello");
let r1 = &s; // 没问题 
let r2 = &s; // 没问题 
let r3 = &mut s; // 大问题 
println!("{}, {}, and {}", r1, r2, r3);

悬挂引用

也就是如果一个引用的指针本来是返回给的,但是还没返回就被释放了,后面接受者得到一个野指针。所谓悬垂指针是其指向的内存可能已经被分配给其他持有者了。

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

fn dangle() -> &String {
    let s = String::from("hello");
    &s  // // 这里 s 离开作用域并被丢弃。其内存被释放。
}


// 错误:expected lifetime parameter

// 解决方法:
fn no_dangle() -> String { 
    let s = String::from("hello"); 
    s 
} // 所有权被移动出去,所以没有值被释放
接下来,我们来看看另一种不同类型的引用:slice。另一个没有所有权的数据类型是 slice。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
 
这里有一个编程小习题:编写一个函数,该函数接收一个字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则整个字符串就是一个单词,所以应
该返回整个字符串。
  分析:first_word 函数有一个参数 &String 。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 部分 字符串的办法。不过,我们可以返回单词结尾
的索引。但是这种方法有个问题,因为存在问题。
fn main() {
    let mut s = String::from("hello world");
    let world = first_word(&s);

    s.clone() // 如果这里将s清除了
    // world还是5,但是s已经变了
}

fn  first_word(s:&String) -> usize {
    let bytes  =s.as_bytes(); // 将String转化为字符数组
    
    for (i,&item) in bytes.iter().enumerate() {  // 使用 iter 方法在字节数组上创建一个迭代器
        // 而 enumerate 包装了 iter 的结果,将这些元素作为元组的一部分来返回。 enumerate 返 回的元组中,
        // 第一个元素是索引,第二个元素是集合中元素的引用
        if item == b‘ ‘ {
            return i;
        }
    }
    s.len()
}

字符串slice
  它是String中一部分值的引用,它看起来像这样:
fn main() {
    let mut s = String::from("hello world");
    let world = first_word(&s);

    println!("ret : {}", world);
}

fn first_word(s: &String) -> &str {
    // 字符串slice的类型声明写作&str
    let bytes = s.as_bytes(); // 将String转化为字符数组

    for (i, &item) in bytes.iter().enumerate() {
        // 使用 iter 方法在字节数组上创建一个迭代器
        // 而 enumerate 包装了 iter 的结果,将这些元素作为元组的一部分来返回。 enumerate 返 回的元组中,
        // 第一个元素是索引,第二个元素是集合中元素的引用
        if item == b‘ ‘ {
            return &s[..i];
        }
    }

    &s[..]
}

更有经验的开发者,可能会这样写函数的签名: fn  first_world(s &str) -> str, 这样如果有一个字符串slice,可以直接传递它,如果有一个String,则可以传递整个String的slice给这个函数,更加舒服。

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b‘ ‘ {
            return &s[0..i];
        }
    }
    &s[..]
}

fn main() {
    let my_string = String::from("hello world");
    // first_word 中传入 `String` 的 slice
    let word = first_word(&my_string[..]);
    let my_string_literal = "hello world"; // first_word 中传入字符串字面值的 slice
    let word = first_word(&my_string_literal[..]); // 因为字符串字面值 **就是** 字符串 slice,
                                                   // 这样写也可以,即不使用 slice 语法!
    let word = first_word(my_string_literal);
}

其他的slice

代码如下:

fn main() {
    let a = [1, 2, 4, 5];
    let slice = &a[1..3]; // 这里的slice是&[i32],它跟字符串slice的工作方式一样,通过存储第一个元素的引用和集合长度,后期讲vector再说

    for i in slice {
        println!("item:{}", i); // 2,4
    }
}

  所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除
其数据的功能意味着你无须额外编写和调试相关的控制代码。所有权系统影响了 Rust 中很多其他部分的工作方式,所以我们还会继续讲到这些概念。

相关推荐