03.认识所有权
所有权系统是Rust语言的核心功能。Rust使用包含特定规则的所有权系统来管理内存,这台规则允许编译器在编译过程中执行检查工作,而不会产生任何的运行时开销。
一、栈(Stack)与堆(Heap)
栈与堆都是代码在运行时可以使用的内存功能。
1、栈(Stack)
栈会以我们放入值时得顺序来存储它们,并以相反得顺序将其取出,这就是所谓得“后进先出”策略;添加数据称为“入栈”,移除数据称为“出栈”。
所有存储在栈中的数据都必须占用已知且固定的大小。
2、堆(Heap)
堆空间的管理是较为松散的。操作系统会根据你的请求在堆中找到一块足够大的可用空间,将它标记为已使用,并把指向这片空间地址的指针返回给我们。
由于多了指针跳转的缓解,所有访问堆上的数据要慢于访问栈上的数据。
3、所有权规则
所有权规则如下:
- Rust中的每一个值都有一个对应的变量作为它的所有者;
- 在同一时间内,值有且仅有一个所有者;
- 当所有者离开自己的作用域时,它持有的值就会被释放掉;
4、变量作用域
作用域是一个对象在程序中有效的范围。
fn main() {
{ // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效
}
5、String类型
String类型会在堆上分配到自己需要的存储空间,所有它能够处理在编译时未知大小的文本。
let s = String::from("hello");
这里的双冒号(::
)运算符允许我们调用置于String命名空间下面的特定from
函数。
6、内存与分配
对于String
类型而言,为了支持一个可变的、可增长的文本类型,我们需要在堆上分配一块在编译时未知大小的内存来存放数据。这也意味着:
- 我们使用的内存是由操作系统在运行时动态分配出来的;
- 当使用完String时,我们需要通过某种方式将这些内存归还给操作系统;
第一步,在调用String::from
时完成,这个函数会请求自己需要的内存空间;
第二步:Rust采用内存会自动地在拥有它的变量离开作用域后进行释放,而在变量离开作用域时,Rust会调用drop
这个特殊函数。
1.移动(move)
fn main() {
let x = 5;
let y = x;
}
将5
绑定到x
;接着生成一个值x
的拷贝并绑定到y
。现在有了变量x
和y
都等于5
,因为整数是有已知固定大小的简单值,所以这两个5
都被放入栈中。
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
}
将hello
绑定到s1
并在堆上存储相关数据,而在栈上则存放s1
指针;当let s2 = s1
时,Rust为了防止出现二次释放内存错误,则将s2
直接指向hello
,而原有的s1
指针被无效;因为只有s2
是有效的,当期离开作用域时,它就释放自己的内存。
2.克隆(clone)
当我们确实需要深度复制String
中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone
的通用函数。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
3.栈上数据的复制
fn main() {
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
}
对于类似于整形的类型可以在编译器时确定自己的大小,并且能够将自己的数据完整地存储在栈中。那么在创建变量y
后,我们没有任何理由去阻止变量x
继续保持有效。
Rust提供了一个名为Copy
的trait
。它可以用于整数这类完全存储在栈上的数据。一般某种类型拥有了Copy这种trait
,那么它的变量就可以在福祉给其他变量之后保持可用性。
不过一般来说,任何简单标量的组合类型都可以是Copy
的,任何需要分配内存或某种资源的类型都不会是Copy
的。
如下是一些 Copy
的类型:
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都实现
Copy
的时候。比如,(i32, i32)
实现了Copy
,但(i32, String)
就没有。
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 移出作用域。没有特殊之处
8、返回值与作用域
返回值也可以转移所有权。
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
println!("The value is : {s3},{s1}"); //输出结果
} // 这里, 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 并移出给调用的函数
}
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop
被清理掉,除非数据被移动为另一个变量所有。
二、引用与借用
引用(references)是Rust提供得一个不用获取所有权就可以使用值得功能。引用是一个地址,我们可以由此访问存储与该地址得属于其他变量得数据。与指针不同,引用确保指定某个特定类型的有效值。
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是String的引用
s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
// 所以什么也不会发生
&
符号就是引用,它允许你使用值但不获取其所有权。
注意:与使用&
引用相反的操作是解引用,它使用解引用运算符*
。
我们将创建一个引用的行为称为借用。当我们尝试修改借用的变量时,是不可以进行的,因为默认情况下不允许修改引用的值。
1、可变引用
当允许我们修改一个借用的值时,这称为可变引用。
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("The value is : {s}")
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
首先,我们必须将s
改为mut
。然后再调用change
函数的地方创建一个可变引用&mut s
,并更新函数签名以接受一个可变引用some_string: &mut String
。这就非常清楚地表明,change
函数将改变它所借用的值。
可变引用存在限制:在同一时间、同一个作用域中,如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用,这是为了防止数据竞争。
2、悬垂引用
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针,所谓悬垂指针是其指向的内存可能已经被分配给其他持有者。相比之下,在Rust中编译器确保引用永远不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
3、引用的规则
- 在任何给定事件,要么只能有一个可变引用,要么只能有多个不可变引用;
- 引用必须总是有效的;
三、切片(Slice)
slice允许你引用集合中一段连续的元素序列,而不引用整个集合。slice是一类引用,所以它没有所有权。
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
可以使用一个由中括号中的[starting_index..ending_index]
指定的range创建一个slice
,其中starting_index
是slice
的第一个位置,ending_index
则是slice最后一个位置的后一个值。在其内部,slice的数据结构存储了slice的开始位置和长度,长度对应于ending_index
减去strating_index
的值。
注意:
字符串slice range的索引必须位于有效的UTF-8字符边界内,如果尝试从一个多字节字符的中间位置创建字符串slice,则程序将会因错误而退出。