// 本页是对RUST第四章的学习汇总记录。
书址
4.1. 什么是所有权
1. 所有权是RUST语言的核心功能(之一)他是一种管理内存的方式
2. **所有权规则
- Rust 中的每一个值都有一个 所有者(owner)。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
3. **变量作用域
fn main() {
{ // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的(进入作用域)
// 使用 s
} // 此作用域已结束,s 不再有效(离开作用域)
}
- 当
s
进入作用域 时,它就是有效的。 - 这一直持续到它 离开作用域 为止。
4. String类型(字符串)
- 这个类型管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本。
- (存储位置大小的文本)这意味着,需要开出未知大小的内存。这需要两个步骤:
- 必须在运行时向内存分配器(memory allocator)请求内存。
- 需要一个当我们处理完String
时将内存返回给分配器的方法。 - 在RUST内存在拥有它的变量离开作用域后就被自动释放。
5. 变量与数据交互的方式(一):移动
fn main() {
let s1 = String::from("hello");
let s2 = s1;
//如果这个时候再次尝试使用s1
println!("{},world",s1);
//此时程序将会报错,因为Rust禁止无效的引用
}
- 在
let s2=s1
之后,s1就不再有效。(Rust 不需要在s1
离开作用域后清理任何东西。) - 有一个特例:
Copy
trait(如果一个类型实现了Copy
trait,那么一个旧的变量在将其赋值给其他变量后仍然可用)可以不使用clone
fn main() {
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);//这是合法的
}
- 任何不需要分配内存或某种形式资源的类型都可以实现
Copy
。如下是一些Copy
的类型:- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都实现
Copy
的时候。比如,(i32, i32)
实现了Copy
,但(i32, String)
就没有
- 所有整数类型,比如
6. 变量与数据交互的方式(二):克隆(clone)
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
- 当出现
clone
调用时,你知道一些特定的代码被执行而且这些代码可能相当消耗资源。你很容易察觉到一些不寻常的事情正在发生。
7. 所有权与函数
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 移出作用域。没有特殊之处
- 变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过
drop
被清理掉,除非数据被移动为另一个变量所有。 - 转移返回值的所有权:
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 转移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 离开作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 会将
// 返回值移动给
// 调用它的函数
let some_string = String::from("yours"); // some_string 进入作用域.
some_string // 返回 some_string
// 并移出给调用的函数
//
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
//
a_string // 返回 a_string 并移出给调用的函数
}
补充:我们可以使用元组来返回多个值
//元组
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)
}
4.2. 引用与借用
引用(reference)像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。(与指针不同,引用确保指向某个特定类型的有效值。)
规则:
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
//引用
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 离开了作用域。但因为它并不拥有引用值的所有权, // 所以什么也不会发生
- 相比较元组:变量声明和函数返回值中的所有元组代码都消失了。
&s1
语法让我们创建一个 指向 值s1
的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。- 我们将创建一个引用的行为称为 借用(borrowing)。
- 默认不允许修改引用的值
可变引用
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_strinig:&mut String
- 如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。尝试对一个变量创建两个可变引用的代码会失败(报错)一个可变引用必须持续到在一个
println!
(或者别的什么)中使用它。(一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。) - 这样子限制的好处是避免数据竞争(这会导致未定义行为,难以在运行时追踪,并且难以诊断和修复):
- 两个或更多指针同时访问同一数据
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
- 也不可以在拥有不可变引用的时候拥有可变引用(不能同时使用可变和不可变引用)
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // '大问题'
println!("{}, {}, and {}", r1, r2, r3);
}
悬垂引用
悬垂指针是其指向的内存可能已经被分配给其它持有者在(Rust 中,编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。)
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!
- 因为
s
是在dangle
函数内创建的,当dangle
的代码执行完毕后,s
将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的String
(s
已经被释放而无效化),这可不对!Rust 不会允许我们这么做。
Slice类型
slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一类引用,所以它没有所有权。
- 编写一个函数,该函数接收一个用空格分隔单词的字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则整个字符串就是一个单词,所以应该返回整个字符串。
- 对于 Rust 的
..
range 语法,如果想要从索引 0 开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
}
- 如果 Slice 包含
String
的最后一个字节,也可以舍弃尾部的数字。这意味着如下也是相同的:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
}
- 也可以同时舍弃这两个值来直接获取整个字符串的Slice,所以如下亦是相同的
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
}
- 字符串字面值就是Slice
- 字符串Slice作为参数:如果有一个字符串 slice,可以直接传递它。如果有一个
String
,则可以传递整个String
的 slice 或对String
的引用。 - 其他类型的Slice:就跟我们想要获取字符串的一部分那样,引用数组的一部分的时候,我们可以这样做:
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
}
总结:
- 所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。