Rust 的核心功能(之一)是 所有权(ownership);
rust 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。
堆 、栈
栈(LIFO)中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。
堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)。
入栈比在堆上分配内存要快,因为(入栈时)操作系统无需为存储新数据去搜索内存空间;其位置总是在栈顶。
所有权规则:
Rust中的每一个值都有一个被称为其 所有者(owner)的变量。- 值有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
内存与分配
字符串自变量和String的核心区别在于: 两个类型对内存的处理上。
自变量形式声明字符串值被硬编码进程序里。字符串字面值是很方便的,不过他们并不适合使用文本的每一种场景。不可变的。并不是所有字符串的值都能在编写代码时就知道。
String 被分配到堆上,所以能够存储在编译时未知大小的文本。
// 字符串自变量的形式声明
fn scope_fn() {
let s = "hello";
println!("{}", s);
}
fn second_string() {
// :: 表示from是string下面的方法
let mut s = String::from("hello");
s.push_str(", world");
println!("{}", s);
}
复制代码
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放,rust就会调用drop方法。
fn main() {
{
let s = String::from("hello"); // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,
} // s 不再有效
复制代码
变量与数据交互的方式:1:移动
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
复制代码
rust 中将变量s1移动到了s2中,这样子也就不存在了内存二次释放的bug。因此s2就是有效的,其离开作用域,释放自己的内存,完毕。
如果你在其他语言中听说过术语 浅拷贝(shallow copy) 和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),而不是浅拷贝。
变量与数据交互的方式:2:拷贝
fn main() {
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
}
复制代码
像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。
Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上,类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。
- 任何简单标量值的组合可以是
Copy的,不需要分配内存或某种形式资源的类型是Copy的 - 所有整数类型,比如
u32。 - 布尔类型,
bool,它的值是true和false。 - 所有浮点数类型,比如
f64。 - 字符类型,
char。 - 元组,当且仅当其包含的类型也都是
Copy的时候。比如,(i32, i32)是 Copy 的,但(i32, String)就不是。
所有权与函数
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 移出作用域。不会有特殊操作
复制代码
有返回值的函数可以转移所有权, 但是rust里面这部分实现是使用引用来实现的。
fn main() {
// let (x, y): (usize, isize) = (1, 2);
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();
// 返回一个元组,
(s, length);
}
复制代码
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
引用和借用
引用(Reference) 是 C++ 开发者较为熟悉的概念。如果你熟悉指针的概念,你可以把它看作一种指针。实质上"引用"是变量的间接访问方式。
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2); // hello hello
}
复制代码
&运算符可以取变量的 "引用"。当一个变量的值被引用时,变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值。
fn main() {
let s1 = String::from("hello");
// &s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。
// 因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
// 函数签名使用 `&` 来表明参数 `s` 的类型是一个引用
fn calculate_length(s: &String) -> usize {
s.len()
}
复制代码
这些 & 符号就是引用,它们允许你使用值但不获取其所有权。
- 引用不会获得值的所有权。
- 引用只能租借(Borrow)值的所有权。
- 引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权
- 禁止修改租借的值,除了
mut修饰过的。也就是可变引用。
可变引用:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!(">>{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
复制代码
限制:在特定作用域中的特定数据有且只有一个可变引用,这个限制的好处是 Rust 可以在编译时就避免数据竞争。**数据竞争(data race)**类似于竞态条件。
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 异常
println!("{}, {}", r1, r2);
复制代码
借用:
我们将获取引用作为函数参数称为 借用(borrowing),正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。
引用总结:、
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的
悬垂引用(Dangling References)
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。
相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
// 解决的办法就是直接返回 return s;
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!
复制代码
slice 切片
切片(Slice) 是对数据值的部分引用。
在 Rust 中有两种常用的字符串类型:str 和 String。
str 是 Rust 核心语言类型,就是本章一直在讲的字符串切片(String Slice),常常以引用的形式出现 &str ,凡是用双引号包括的字符串常量整体的类型性质都是 &str。
let s = "hello";
复制代码
Rust 中的字符串类型实质上记录了字符在内存中的起始位置和其长度。
fn main() {
let s = String::from("broadcast");
let part1 = &s[0..5]; // 前闭后开的一个结构
let part2 = &s[5..9];
println!("{}={}+{}", s, part1, part2);
}
复制代码
一些常见的省略写法:
..y 等价于 0..y
x.. 等价于位置 x 到数据结束
.. 等价于位置 0 到结束
复制代码
被切片引用的字符串禁止更改其值, 因为只是部分引用, 切片结果必须是引用类型,但开发者必须自己明示这一点:
fn main() {
let s = String::from("broadcast");
let slice = &s[0..3];
s.push_str('aa'); // 错误异常
println!("slice>>> {}", slice);
}
复制代码
非字符串切片:
除了字符串以外,其他一些线性数据结构也支持切片操作,例如数组:
fn main() {
let arr = [1, 2, 3, 4, 5, 6];
let part = &arr[..3];
for i in part.iter() {
println!(">>>{}", i);
}
}
复制代码




近期评论