写点什么

5 分钟速读之 Rust 权威指南(五)

用户头像
码生笔谈
关注
发布于: 2021 年 05 月 19 日
5分钟速读之Rust权威指南(五)

所有权

所有权的概念是与一般的编程语言最大的不同,rust 整个语言的设计都是围绕所有权来的,在学习所有权的时候需要反复练习,去体会其中的设计,书中总结了三个原则:


  • Rust 中的每一个值都有一个对应的变量作为它的所有者。

  • 在同一时间内,值有且仅有一个所有者。

  • 当所有者离开自己的作用域时,它持有的值就会被释放掉。

变量作用域

  • x 在进入作用域后变得有效。

  • 它会保持自己的有效性直到自己离开作用域为止。


fn main() {  println!("{}", x); // 报错,x还未初始化  { // 这里是一个作用域的开始    let x = "string";    println!("{}", x); // "string"  } // 这里是一个作用域的结束,x将被回收  println!("{}", x); // 报错,x不存在}
复制代码

String 类型

字符串的堆存储和栈存储

初始化一个存放在堆中的字符串:


// 创建空字符串let mut s1 = String::new();
// 从字面量创建字符串let mut s2 = String::from("hello");
// 追加单个chars1.push('h');
// 可以追加一个字符串s2.push_str("world");
println!("{}-{}", s1, s2); // h-helloworld
复制代码


初始化一个存放在栈中的字符串:


// 字面量声明的字符串会存在栈中,是不可变的let mut x = "string";x.push('x'); // 报错: 不存在 'push' 方法
复制代码

所有权转移

存放在堆中的数据会发生所有权的转移:


// 初始化一个堆中的字符串let s1 = String::from("1");
// 这里将s1的指针转移给了s2,s1会被释放let s2 = s1;println!("{}", s2) // "1"
// 尝试获取s1println!("{}" s1) // 报错,s1的数据已经移动给了s2
复制代码


栈中的数据会发生复制,而不会转移所有权:


// 初始化一个栈的字符串let s1 = "hello";
// s2会复制给s1let s2 = s1;
// 获取s1和s2println!("{}" s1); // "hello"println!("{}" s2); // "hello"
复制代码


使用数据克隆复制堆中数据:


let s1 = String::from("hello");
// 复制s1的数据给s2let s2 = s1.clone();
// 获取s1和s2println!("{}" s1); // "hello"println!("{}" s2); // "hello"
复制代码


存在栈中的数据类型,是否使用 clone 是没有区别的:


let s1 = "hello";
// 复制s1的数据给s2let s2 = s1.clone();// 等价于// let s2 = s1;
// 获取s1和s2println!("{}" s1); // "hello"println!("{}" s2); // "hello"
复制代码


事实上上面只是拿字符串来举例,对于数字和 char 来说,也是存在栈中的,同样会进行数据复制,而不是所有权的转移。

关于元组

如果元组中所有成员都是栈中数据,那元组同样可以复制:


// 定义一个包含字符串,char,数字类型的元组let t1 = ("abc", 'c', 1);
// t1复制给t2let t2 = t1;
// 尝试获取println!("{:?}", t1); // ("abc", 'c', 1)println!("{:?}", t2); // ("abc", 'c', 1)
复制代码


如果成员中有一个数据不是可复制的,那么元组也不可能复制,只是简单的所有权转移:



// 初始化一个包含堆中数据的元组let t1 = ("abc", 'c', String::new());
// 这里t1会把所有权转移给t2let t2 = t1;
// 尝试获取println!("{:?}", t2);println!("{:?}", t1); // 报错,t1的所有权已经移动给了t2
复制代码


允许移动成员,但是移动后不能再使用:


let t1 = (String::from("hello"), String::from("world"));
// 将第一个成员的所有权转移给slet s = t1.0;
println!("{}", s); // "hello"println!("{}", t1.1); // "world"println!("{:?}", t1); // 报错,成员的所有权已经被移动了println!("{}", t1.0); // 报错,成员的所有权已经被移动了
复制代码


数组成员如果是栈中数据,那数组同样可以复制:


let t1 = (1, 2);
// 复制第一个成员给t2let t2 = t1.0;
println!("{}", t2); // 1println!("{}", t1.0); // 1println!("{}", t1.1); // 2
复制代码


数组的规则与元组的规则相同,这里不再介绍,可以自行练习

所有权与函数参数

调用函数时,对函数的传参也会转移所有权:


// 由于函数参数类型是堆中数据,所以会发生所有权转移fn take_ownership(s: String) {  println!("{}", s) // "hello"}
// 定义一个堆中数据let s = String::from("hello");
// 调用函数会将s的所有权转移给函数take_ownership(s);
// 尝试获取sprintln!("{}", s); // 报错,所有权被take_ownership拿走了
复制代码


栈中的数据会发生复制:


// 由于函数参数类型是栈中数据,所以会发生复制fn makes_copy(n: i32) {   println!("{}", n) // 5}
// 定义一个栈中数据let num = 5;
// 调用函数会对num进行复制makes_copy(num);
// num仍然有效println!("{}", num); // 5
复制代码

返回值与作用域

当一个持有堆数据的变量离开函数作用域时,它的数据就会被清理回收,但是可以通过返回值将数据返回而不被销毁:


// 函数中创建一个堆中数据字符串,并返回所有权fn gives_owner_ship() -> String {  let s = String::from("hello");  s}
// 函数接受一个字符串数据的所有权,然后直接返回fn takes_and_gives_back(s: String) -> String { s}
// 获取函数中创建的字符串的所有权let s1 = gives_owner_ship();
// 将s2的所有权转移给函数,再通过函数返回值将所有权转移给s3let s2 = String::from("world");let s3 = takes_and_gives_back(s2);
println!("{}", s1); // "hello"println!("{}", s3); // "world"println!("{}", s2); // 报错, s2的所有权转移到了s3
复制代码


利用返回值拿回所有权:


fn get_lenth(s: String) -> (String, usize) {  let len = s.len(); // 获取s的长度  (s, len) // 将s的所有权和长度返回}
let s1 = String::from("hello");
// 使用返回值重新拿到所有权let (s1, len) = get_lenth(s1);
println!("{}-{}", s1, len); // "hello-5"
复制代码

引用变量

数据的所有权转移会降低开发的灵活性,所有有了引用的概念来解决这个问题:


let s1 = String::from("hello");
// 使用&符号创建两个对s1的引用,不会获取s1的所有权,可以将引用理解成一个指针let s2 = &s1;let s3 = &s2;
// 获取println!("{}-{}-{}", s1, s2, s3); // "hello-hello-hello"
复制代码


函数同样支持引用类型:


// 指定引用类型的参数fn get_length(s: &String) -> usize {  // 这里的 s 指向 s1 指向 String真实数据  // 由于s没有取得String数据的所有权,所以这里不需要使用返回值归还所有权  s.len()}
let s1 = String::from("hello");
// 将s1的引用传给函数let len = get_length(&s1);
// s1仍可使用println!("{}-{}", s1, len); // "hello-5"
复制代码


使用 &可以引用数据,但是不可以对数据进行改变:


let s1 = String::from("hello");let s2 = &s1;
// 通过s2试图改变数据s2.push("world"); // 报错,s2是一个不可变引用,不能对数据进行更改
复制代码


使用可变引用对数据进行更改:


// 使用&mut将参数声明为可变引用fn get_length(s: &mut String) -> usize {  s.push_str("world");  s.len()}
// 声明可变数据let mut s1 = String::from("hello");
// 传入可变引用变量let len = get_length(&mut s1);
// s1发生了数据变更println!("{}-{}", len, s1); // "10-helloworld"
复制代码


对于单个作用域中的数据来说,一次只能声明一个可变引用:


let mut s1 = String::from("hello");let s2 = &mut s1;let s3 = &mut s1 // 报错,不能有多个可变引用
复制代码


在单个作用域中也不能存在可变引用和可变引用的函数传参:


let mut s1 = String::from("hello");let s2 = &mut s1;let len = get_length(&mut s1); // 报错,同样不能有多个可变引用,因为传参和s2都属于可变引用
复制代码


可变与不可变引用不能同时存在:


let mut s1 = String::from("hello");
// 创建不可变引用let s2 = &s1; // 报错,不能将s1进行不可变引用,因为下边有可变引用
// 创建可变引用let mut s3 = &mut s1; // 报错,不能将s1进行可变引用,因为上边有不可变引用
// 前提是下面有使用到上边的值,否则就不会报错println!("{}", s1);println!("{}", s2);println!("{}", s3);
复制代码

引用的规则

  • 在任何一段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用,试想一下如果数据改变了,那么不可变的变量的不可变特性是否还有意义。。

  • rust 会保证引用总是有效的。

发布于: 2021 年 05 月 19 日阅读数: 11
用户头像

码生笔谈

关注

欢迎关注「码生笔谈」公众号 2018.09.09 加入

前端、Rust干货分享,一起成为更好的Javascripter & Rustacean

评论

发布
暂无评论
5分钟速读之Rust权威指南(五)