所有权
它有个独特的特性,就是没有 GC
就可以保证内存安全。
什么是所有权?
- 所有程序在运行时都必须管理它们使用计算机内存的方式
- NodeJS 垃圾收集机制,在程序运行时,就会不断地寻找不再使用的内存,老生代和新生代的内存。
- C/C++,程序员必须显式地分配和释放内存。
- Rust 通过所有权系统管理内存,其中包含一组编译器在编译时检查的规则。当程序运行时,所有权特性不会减慢程序的运行速度。
Rust 的核心特性就是所有权。
Stack VS Heap
Stack
- 把值压到
Stack
上不叫分配。 - 储存在
Stack
上的数据必须是已知的固定大小的,如果想要实际数据,我们必须使用指针来定位。 - 把数据压到
Stack
上要比在Heap
上分配快得多,因为Heap
多了一个指针跳转的环节,属于间接的访问。- 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在
Stack
的顶端。
- 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在
- LIFO 后进先出。
- 把值压到
Heap
- 未知数据大小 未知运行时大小的数据放在
Heap
内存。 - 把数据存在
Heap
时,会请求一定数量的空间。 - 操作系统在
Heap
找到一块足够大的空间,并且标记为在用,而且返回一个指针,它就是这个空间的地址。 - 这个过程叫做在
Heap
上进行分配,有时候仅仅成为 “分配”。
- 未知数据大小 未知运行时大小的数据放在
为什么 Stack 比 Heap 快
- 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远在
Stack
的顶端。
- 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远在
- 在
Heap
上分配空间需要做更多的工作:
- 操作系统首先需要找到一块足够大的空间来存储数据,然后要做好记录方便下次分配。
- 在
- 访问
Heap
的数据要比访问Stack
中的数据慢,因为需要通过指针才能找到Heap
中的数据。
- 现代处理器由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快。
- 访问
- 如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(
Stack
)。
- 如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(
- 如果数据存放的距离比较远,那么处理器的处理速度就会慢(
Heap
)。
- 在
Heap
上分配大量的空间也是需要时间的。
- 如果数据存放的距离比较远,那么处理器的处理速度就会慢(
函数调用
当你的代码调用函数时,值被传入到函数(也包括指向 Heap
的指针)。函数本地的变量被压到 Stack
上。当函数结束后,这些值会从 Stack
上弹出。
在 Rust 这样的系统级编程语言里,一个值是在 stack
上还是在 heap
上对语言的行为和你为什么要做某些决定是有更大影响的。
所有权的规则
- 每个值都有一个变量,这个变量是该值的所有者。
- 每个值同时只能有一个所有者。
- 当所有者超出作用域(Scoped)的时候,该值将被删除。
变量作用域
Scope
就是程序中一个项目的有效范围。
fn main(){
// name 不可用
let name = "ACE"; // name 可用
// 可以操作 name
} // 作用域结束 name 不再可用
所有权存在的原因
- 所有权解决的问题:
- 跟踪代码的哪些部分正在使用
Heap
的哪些数据。 - 最小化
Heap
上的重复数据量。 - 清理
Heap
上未使用的数据以避免空间不足。
- 跟踪代码的哪些部分正在使用
如果懂了所有权就不会去想存在 Heap
还是 Stack
上了。
fn test_fn(){
let str = String::from("hello world");
str // 函数在返回值的过程中也会发生所有权转移
}
let test = test_fn(); // 这就是所有权由 test_fn 作用域转移到了 test
💡 注意:当一个包含 Heap
数据的变量离开作用域的时候,它的值就会被 drop
函数清理,除非数据的所有权移动到另一个变量上了。
💡 注意:slice 不含有
Copy
浅拷贝 shallow Copy
深拷贝 deep Copy
你也许会将复制指针、长度、容量视为浅拷贝,但由于 Rust 让 s1 失效了,所以要用到一个新的术语:移动(Move)
- 原来 s1 指向 箭头右侧的
Heap
内存,然后 s1 赋给 s2,于此同时 s1 就失效了就变成灰色了,并且此时只有 s2 是有效的。 - 也就是说 s1 被移动到了 s2 身上,既然只有 s2 是生效的,所以只有它离开自己作用域的时候释放内存空间,所以也就不会再发生二次释放内存的可能性了,所以这也就体现出了
Rust
的安全性。 - 这里也隐含了一个设计原则:
Rust
不会自动创建数据的深拷贝。- 也就是说,就运行时的性能而言,任何自动赋值的操作都是廉价的。
Clone
- 如果真想对 Heap 上面的 String 数据进行深度拷贝,而不仅仅是 String 数据进行深度拷贝,而不仅仅是 Stack 上的数据,可以使用 Clone 方法。
fn main(){
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{},{}", s1,s2);
}
这种方式比较消耗资源,主要针对堆上面的数据
复制
针对 Stack 上的数据实际上不需要 Clone ,叫复制就可以了
fn main(){
let x = 5;
let y = x;
println!("{}, {}", x, y);
}
上面的代码并没有使用 clone
这个方法,在 main
函数里面的第 3 行之后,x,y 都是有效的,这是因为 x 是整数类型,而且整数类型在编译的时候就确定了自己的大小
并且能将自己的数据完整的存储到Stack
上,而对于这些值的复制操作永远都是非常快速的,这也同样的意味着在创建变量外之后我们没有任何理由去阻止变量 x 继续去保持有效。
就是对于这些类型而言深拷贝和浅拷贝是没有任何区别的调用 clone
这个方法并不会与直接的浅拷贝有直接行为上的区别,因此我们不需要在类似的场景考虑上面的问题。
Copy Trait
(接口),可以用于像整数那样完全存放在Stack
上面的数据类型- 如果一个类型实现了
Copy
这个Trait
,那么旧的变量在赋值后仍然可用 - 如果一个类型或者该类型的一部分实现了
Drop Trait
,那么 Rust 不允许让它再去实现Copy Trait
了,否则编译就会进行报错了。
一些拥有 Copy Trait
的类型
- 任何简单标量的组合类型都是可以 Copy 的
- 任何需要分配内存或者某种资源的都不是 Copy 的
- 一些拥有
Copy Trait
的类型- 所有的整数类型,例如 u32
- bool
- char
- 所有的浮点类型,例如 f64
- Tuple(元组),如果其所有的字段都是可以 Copy 的,那么它也是拥有
Copy Trait
的类型- (i32, i32) 是
- (i32, String) 不是
所有权 与 函数
- 在语义上,将值传递给函数和把值赋给变量是类似的:
- 将值传递给函数将发生移动或者复制
fn main() {
let s = String::from("hello world");
take_ownership(s); // 这个时候 s 这个值就等于移动到 take_ownership函数 内部了,从这以后 s 就不再有效了。
// println!("{}", s); // 所以这行打开注释就会报错 borrow of moved value: `s`
let x = 5;
makes_copy(x); // 由于 x 是个 i32 类型,而 i32 这个类型实现了 Copy Trait ,所以传递给函数 makes_copy 实际上是 x 的副本,所以 x 在这行之后仍然有效
println!("x:{}", x);
} // 在这一行 s 和 x 也就离开了作用域全部失效,但是由于之前 s 发生了移动,所以不会发生其他的事情
fn take_ownership(some_string: String) {
println!("{}", some_string);
} // 在这一行 Rust 会自动调用 drop 这个函数,some_string 所占用的内存就被释放了
fn makes_copy(some_number: i32 // 这个 some_number 参数实际上是源数据的副本) {
println!("{}", some_number);
} // 在这一行 Rust 会自动调用 drop 这个函数,some_number 所占用的内存就被释放了
返回值 与 作用域
函数在返回值的过程中同样也会发生所有权的转移
fn main() {
//
let s1 = gaves_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
}
fn gaves_ownership() -> String {
let some_string = String::from("hello");
some_string
} // some_string 作为一个返回值移动至了 调用它的函数里面,也就是 main 函数里面, 实际上就是把 some_string 移动给了 s1
fn takes_and_gives_back(a_string: String) -> String {
a_string
} // 这个函数的作用就是获取了 string 的所有权并将它作为结果进行返回,而这个函数的返回值又被移动到了 s3 上
- 一个变量的所有权总是遵循同样的模式:
- 把一个赋值给其它变量时就会发生移动
- 当一个包含
heap
数据的变量离开作用域时,它的值就会被 drop 函数清理,除非数据的所有权移动到另一个变量上了才不会被清理。
切片
Rust 的另外一种不持有所有权的数据类型:切片(slice)
fn main() {
let s = String::from("Hello World");
let wordIndex = first_world(&s);
// s.clear();
println!("{}", wordIndex);
}
fn first_world(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
println!("{}", i);
println!("{}", item);
if item == b' ' {
return i;
}
}
s.len()
}
- 如果调用了
s.clear
方法把字符串清空。由于wordIndex
和 字符串s
本身没任何关联,所以即使这个字符串被清空了,那么wordIndex
值仍然是 5,所以它就毫无意义,而此时我们 想用 wordIndex 来提取字符串第一个单词就会发生错误引起 BUG。 所以 first_world 这样的函数设计必须随时关注 wordIndex 的有效性,需要确保索引 和 String 变量 s 他们之间 的同步性,所以这类工作往往是相当繁琐,而且特别容易出错,而针对这类问题 Rust 就提供了解决方案字符串切片
。
字符串切片
是指向字符串中一部分内容的引用
形式:[开始索引..结束索引]
- 开始索引就是切片其实位置的索引值
- 结束索引是切片终止位置的下个索引值
图中的 s 就是个 string
类型,箭头左边它是放在 Stack
上的,内容是放在 Heap
上面的也就是箭头右边,但是上图有点问题, len
长度和 capacity
容量应该是 11 才对, world
就是个字符串的切片
从 6 切到 11。
fn main() {
let s = String::from("Hello World");
let hello = &s[..5];
let world = &s[6..];
println!("{} {}", hello, world);
let whole = &s[0..s.len()];
let whole1 = &s[..];
println!("{} {}", whole, whole1);
}
- 字符串切片的范围索引必须发生在有效的 UTF-8 字符边界内
- 如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出。
fn main() {
let s = String::from("Hello World");
let wordIndex = first_world(&s);
s.clear();
println!("{}", wordIndex);
}
fn first_world(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
// 错误提示
Compiling lesson5 v0.1.0 (D:\Rust\lesson5)
error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
--> src\main.rs:6:5
|
2 | let s = String::from("Hello World");
| - help: consider changing this to be mutable: `mut s`
...
6 | s.clear();
| ^^^^^^^^^ cannot borrow as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `lesson5` due to previous error
字符串字面值
字符串字面值实际上就是切片
- 字符串字面值被直接存储在二进制程序中
- let s = "Hello World";
- 变量 s 的类型是 &str,它是一个指向二进制程序特定位置的切片
- &str 是不可变引用,所以字符串字面值也是不可变的
将字符串切片作为参数传递
- fn first_world(s: &String)-> &str {}
- 有经验的
Rust
工程师会采用&str
作为参数类型,因为这样就可以同时接收String
和&str
类型的参数了; - fn first_world(s: &str)-> &str {}
- 使用字符串切片,直接调用该函数
- 如果传入的是
String
类型的参数,可以创建一个完整的String
切片来调用该函数
- 定义函数时使用字符串切片来代替字符串引用会使我们
API
更加通用,且不会损失任何功能
fn main() {
let my_string = String::from("Hello World");
let wordIndex = first_world(&my_string[..]);
let my_string_literal = "Hello World";
let wordIndex = first_world(my_string_literal);
println!("{}", wordIndex);
}
fn first_world(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
- 数组切片
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
// 这个跟字符串切片一样,这个里面存储了一个指针,这个指针指向了起始元素的位置,他还存储一个长度,长度是2
}