# stack vs. heap
stack 速度比 heap 快
所有权解决的问题:
- 跟踪代码哪些部分正在使用 heap 的哪些数据
- 最小化 heap 上的重复数据量
- 清理 heap 上未使用的数据,以避免空间不足
管理 heap 数据是所有权的主要原因
# 所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域(scope)时,该值将被删除。
# 变量作用域
- scope 就是程序中一个项目的有效范围
# 所有权举例:String 类型
- String 比那些基础标量数据类型更复杂
- 字符串字面值:程序里手写的那些字符串值。不可变。
- String 在 heap 创建
# 创建 String 类型的值
使用 from 函数从字符串字面值创建出 String 类型
let mut s = String::from("Hello");
这类字符串是可以被修改的
内存:
- 内存不释放就浪费内存
- 提前释放,变量就非法
- 释放多次就导致 bug,因此必须一次分配对应一次释放
# 内存和分配
- rust 对于某个值来说,当拥有它的变量走出作用范围时,内存就会立即自动交还给操作系统
- drop 函数
# 变量和数据交互的方式:移动(move)
- 多个变量可以与同一个数据使用一种独特的方式来交互
let x = 5;
let y = x;
这 2 个变量的值压入到 stack 中
# 变量和数据交互的方式:移动,String 版本
fn main() { | |
let s1 = String::from("hello"); | |
let s2 = s1; | |
// 这里会报错; | |
// 因为 s1 的值所有权已经移动到 s2 了; | |
//s1 失效,不能再使用了 | |
println!("{}", s1); // error. | |
} |
# 浅拷贝、深拷贝和移动
浅拷贝(shallow copy)和深拷贝(deep copy)是其他语言的概念。
浅拷贝导致内存得不到释放或者多次释放。
rust 不使用浅拷贝这种概念,rust 使用移动(move)概念;
例如上面的 s1 和 s2;s2 = s1 会把 s1 的值移动到 s2;数据的所有权移动到了 s2,且 s1 失效。
rust 也不会创建深拷贝,移动操作都是很廉价、快速的。
# 变量和数据交互的方式:克隆(Clone)
如果真的需要深拷贝,可以使用 clone 方法,这里不使用深拷贝概念。
浅拷贝对应 rust 的移动概念,rust 多了一步失效前一个变量的操作。
深拷贝对应 rust 的克隆概念。
以上移动和克隆都是对 heap 上的数据讨论的。stack 上的数据直接复制就 ok。
fn main() { | |
let s1 = String::from("hello"); | |
let s2 = s1.clone(); | |
println!("{} {}", s1, s2); | |
} |
# stack 上的数据:复制
fn main() { | |
let x = 5; | |
let y = x; | |
// 不报错,这里不发生移动 | |
//stack 执行的是复制操作 | |
//stack 数据都是确定的,所以可以自动计算 | |
println!("{} {}", x, y); | |
} |
- Copy trait: 可以像整数一样,完全放在 stack 上面的类型。
- 如果一个类型实现了 Copy 这个接口 (trait,特性),那么旧的变量在赋值后仍然可以使用。因为发生的是复制 (Copy),而不是移动。
- 因此,如果一个类型或者该类型的一部分实现了 Drop trait,那么 rust 不允许再实现 Copy trait。因为复制和移动这 2 个概念是冲突的,因此不能同时实现。
# 一些拥有 Copy trait 的类型
- 任何简单的标量的组合类型都可以 Copy 的
- 任何需要分配内存或者某种资源的都不是 Copy 的
- 一些拥有 Copy trait 的类型:
- 所有整数类型,例如 u32
- bool
- char
- 所有的浮点类型,例如:f64
- Tuple(元组),如果其所有的字段都是 Copy 的:(i32, i32) 是,而 (i32, String) 不是
# 所有权与函数
将值传递给函数和把值赋值给变量是类似的:
- 将值传递给函数会发生移动或复制
fn main() { | |
let s = String::from("Hello World"); | |
//s 的值在堆中,值的所有权移动到函数内部了 | |
// 函数结束后,因为持有所有权,因此就会释放 | |
// 函数后面就不能再使用 s1 变量了。 | |
take_ownership(s); | |
let x = 5; | |
// 因为 x 是标量,值在 stack 中; | |
//i32 实现了 Copy trait | |
// 传入函数内部发生复制,因此函数后面 x 还能使用 | |
makes_copy(x); | |
println!("{}", x); | |
} | |
fn take_ownership(some_string: String) { | |
//some_string 获得所有权 | |
println!("{}", some_string); | |
} // 函数结束,释放 some_string 堆上的内存 | |
fn makes_copy(some_number: i32) { | |
//some_number 通过复制获得新的拷贝值 | |
println!("{}", some_number); | |
} // 函数结束,some_number 在 stack 内存上自动释放 | |
// 因为是复制的,不会影响外部的变量 |
# 返回值与作用域
函数的返回值的过程中同样也会发生所有权的转移
fn main() { | |
// 从函数返回值中获得所有权 | |
let s1 = gives_ownership(); | |
let s2 = String::from("hello"); | |
// 传入 s2 变量,所有权移动到函数内部 | |
// 函数直接返回,再次将所有权移动到函数外面的 s3 | |
let s3 = takes_and_gives_back(s2); | |
// 后面不能使用 s2 变量,因为所有权移动到 s3 上了 | |
//s2 -> a_string(函数入参) -> s3 | |
} | |
fn gives_ownership() -> String { | |
let some_string = String::from("hello"); | |
some_string | |
} // 返回值移动所有权到函数调用者 | |
fn takes_and_gives_back(a_string: String) -> String { | |
a_string | |
} // 传入参数获得所有权,直接返回,返回值移动所有权到函数调用者 |
# 如何让函数使用某个值,但不获取其所有权?引用
fn main() { | |
let s1 = String::from("hello"); | |
// 为了获取字符串的长度,需要传入变量 | |
// 但这样传入发生所有权的移动,而且发生 2 次移动 | |
// 有点啰嗦和影响性能 | |
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) | |
} |
函数传入参数,又返回此参数;
所有权移动了 2 次,但我们可能只想使用,但不需要获取其所有权,因此引入了引用(Reference)和借用(Borrow)的概念。
需要区分所有权、引用 / 借用的概念,这 2 个概念容易混淆,但不是同一个东西。
引用 / 借用不会改变所有权,函数结束后,也不会释放变量(没所有权)。
# 引用与借用
fn main() { | |
let s = String::from("hello"); | |
// &s 这里是引用 | |
let length = calculate_length(&s); | |
println!("The length of '{}' is {}.", s, length); | |
} | |
// 这里 s 是借用;只有借用关系,不会发生所有权的移动 | |
fn calculate_length(s: &String) -> usize { | |
s.len() | |
} |
引用和借用是同一个东西,只不过发生的位置不同,传入参数就是引用的概念;函数参数定义就是借用的概念。
# 借用
和变量一样,引用默认也是不可变的。
如果借用一个值,需要修改,则需要定义为可变引用,变量也要定义为可变。
fn main() { | |
// 变量需要定义为可变 | |
let mut s = String::from("hello"); | |
// 引用需要 &mut 可变引用 | |
let length = calculate_length(&mut s); | |
println!("The length of '{}' is {}.", s, length); | |
} | |
// 函数参数需要定义为可变借用 | |
fn calculate_length(s: &mut String) -> usize { | |
s.push_str(" World"); | |
s.len() | |
} |
# 可变引用
- 可变引用有一个重要的限制:在特定作用域内,对某一块数据,只能有一个可变的引用。
- 这样做的好处是可在编译时防止数据竞争。
以下三种行为下(同时存在)会发生数据竞争:
- 两个或多个指针同时访问同一个数据
- 至少有一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
fn main() { | |
let mut s = String::from("hello"); | |
let s1 = &mut s; | |
let s2 = &mut s; | |
//s1 和 s2 会发生数据竞争,因此编译不通过 | |
println!("{} {}", s1, s2); | |
} |
可以通过创建新的作用域,来允许非同时的创建多个可变引用
fn main() { | |
let mut s = String::from("hello"); | |
{ | |
let s1 = &mut s; | |
} | |
let s2 = &mut s; | |
} |
# 另外一个限制
- 不可以同时拥有一个可变引用和一个不可变引用
- 多个不变引用是可以的
可变引用会修改数据,如果没有同步操作,不可变引用的值可能被改变导致数据不一致;因此 rust 限制这种情况。
fn main() { | |
let mut s = String::from("hello"); | |
let r1 = &s; | |
let r2 = &s; | |
// 不能同时有可变和不可变的引用 | |
let s1 = &mut s; | |
println!("{} {} {}", r1, r2, s1); | |
} |
# 悬空引用 Dangling References
悬空指针(Dangling Pointer): 一个指针引用了内存中的某一个地址,而这块内存可能已经释放并分配给其它人使用了。
Rust 中,编译器可以保证引用永远都不是悬空引用。
在 rust 中,只有这种情况可能发生悬空引用:
- 函数内部创建一个对象,然后仅仅返回其引用;然而函数结束后;由于函数内持有所有权,结束会释放内存,但返回引用给调用者;这样就发生悬空引用。
- rust 不会允许这种情况发生,需要指定生命周期,扩大对象的作用域范围。
fn main() { | |
let r = dangle(); | |
} | |
fn dangle() -> &String { | |
let s = String::from(hello""); | |
&s | |
} |
# 引用的规则
- 在任何给定的时刻,只能满足下列条件之一:
- 只有一个可变的引用
- 或者只能任意数量的不可变的引用
- 引用必须一直有效
# 切片
rust 的另外一种不持有所有权的数据类型:切片 (slice)
以下程序可能有什么问题?
fn main() { | |
let mut s = String::from("Hello World"); | |
let word_index = first_word(&s); | |
// 如果 s 的内存被清空了,但还有 index 在; | |
// 后面使用 index 从 s 中获取内容, | |
// 就导致 bug,例如下面这句; | |
s.clear(); | |
println!("{}", word_index); | |
} | |
fn first_word(s: &String) -> usize { | |
let bytes = s.as_bytes(); | |
for (i, &item) in bytes.iter().enumerate() { | |
if item == b' ' { | |
return i; | |
} | |
} | |
s.len() | |
} |
# 字符串切片
字符串切片是指向字符串中一部分的内容引用
注意需要 & 符号,因为是一个引用,类型是 &str。
形式:& 变量 [开始索引.. 结束索引],不包含结束索引
注意:字符串切片范围索引要是有效的 utf-8 字符边界内,否则程序会报错退出。
fn main() { | |
let s = String::from("hello world"); | |
let hello = &s[0..5]; // 语法糖:&s [..5]; | |
let world = &s[6..11]; // 语法糖:&s [6..]; 或 &s [6..s.len ()]; | |
let helloworld = &s[..]; // 语法糖 | |
} |
# 使用字符串切片重写例子
fn main() { | |
let mut s = String::from("Hello World"); | |
let word_index = first_word(&s); | |
// 这里由于所有权会报错,因此 rust 机制保证安全, | |
// 防止内存释放后,还使用相关内容 | |
s.clear(); | |
println!("{}", word_index); | |
} | |
fn first_word(s: &String) -> &str { | |
let bytes = s.as_bytes(); | |
for (i, &item) in bytes.iter().enumerate() { | |
if item == b' ' { | |
return &s[..i]; | |
} | |
} | |
&s[..] | |
} |
# 字符串字面值是切片
- 字符串字面值直接被存储在二进制程序中。
- 变量 s 的类型是 &str,它是一个指向二进制程序特点位置的切片。
- 字面值不可变。
fn main() { | |
let s = "Hello World"; // 相当于 &str 类型 | |
println!("{}", s); | |
} |
# 将字符串切片作为参数传递
建议函数入参不使用 &String,更通用的做法是使用切片作为入参。
fn main() { | |
let mut s = String::from("Hello World"); | |
let word_index = first_word(&s[..]); | |
println!("{}", word_index); | |
// 通用 | |
let word_index = first_word("Hello World"); | |
println!("{}", word_index); | |
} | |
fn first_word(s: &str) -> &str { | |
let bytes = s.as_bytes(); | |
for (i, &item) in bytes.iter().enumerate() { | |
if item == b' ' { | |
return &s[..i]; | |
} | |
} | |
s | |
} |
# 其它类型的切片
例如数组 i32 的切片,类型是 &[i32]
fn main() { | |
// 类型 &[i32] | |
let a = [1, 2, 3, 4, 5]; | |
let slice = &a[1..3]; | |
println!("{} {}", slice[0], slice[1]); | |
} |