Hello Rust [2] - 所有权
所有权
栈和堆
所有权的目的主要目的就是为了管理堆数据。
- 栈:后进先出,必须占用已知且固定大小空间。
- 堆:缺乏组织,需要内存分配器分配内存。可以保存大小变化或者位未知的数据。
栈与堆的比较:
- 入栈的速度比堆的分配内存更快,因为不需要搜索内存空间。
- 栈的访问比堆快。
所有权规则
- Rust中的每一个值都有一个被称为 所有者(owner) 的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者离开作用域,该值被丢弃。
变量作用域
fn main() {
{ // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效
}Ï
- s进入作用域的时候,是有效的,直到持续到它离开作用域。
String,内存,分配
{
// 从String命名空间声明一个可变的字符串s
let mut s = String::from("hello");
// 使用 s
} // 作用域结束,s失效。
对于这个String类型:
- 需要在运行时,向内存分配器请求内存(memory allocator)方法
- 处理完数据需要释放(GC)
RUST的GC是基于所有权的。所以s离开作用域的时候,rust会调用一个特殊的函数drop,String的作者可以防止释放内存的代码,Rust会在结尾的}自动调用drop。
在C++中,这种“在生命周期结束时释放资源的模式”有时候被称为资源获取即初始化(RAII)模式。
移动
当两个数据指针指向同一个堆地址,可能会尝试释放相同的内存,这就是 二次释放(double free) 错误。所以,Rust会失效掉比较早的一个数据指针。这就是所谓的 移动(move)。
let s1 = String::from("hello");
let s2 = s1; // s1 被指向了 s2
println!("{}, world!", s1);
不同于深拷贝和浅拷贝, RUST会使第一个变量无效。同时只有s2是生效的。
同时,Rust永远也不会自动创建数据的“深拷贝”, 自动的复制对运行时的影响都比较小。
克隆 & 拷贝
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
在堆中的数据,你需要调用clone去克隆他们,在栈中,则会自动进行拷贝。
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // x, y 都是可以访问的。
如果一个类型实现了Copy trait,则会在复制的时候进行拷贝。
默认的,我们有这些类型实现了Copy:
- 所有整数
- 布尔类型
- 浮点数
- 字符类型
- 元组
函数
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 移出作用域。没有特殊之处Ï
在takes_ownership后调用s会抛出一个编译错误。
返回值与作用域
返回值也可以转移所有权:
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 并移出给调用的函数
}
变量的所有权都是遵循相同的模式:赋值给听一个变量的时候移动它。当持有堆中数据量的变量离开作用域的时候,其值将通过drop被清理掉。除非,被移动到另一个变量。
除了这种方式,Rust还是提供了另一个方式来不用获取所有权就可以使用值的能力:引用(references)。
引用与借用
- 引用(reference):创建一个指向变量的指针。
- 借用(borrowing):创建一个引用的行为叫做借用。
fn main() {
// 声明了一个堆中的字符串变量
let s1 = String::from("hello");
// 我们给了s1的引用,但是不转移所有权。
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
// 函数定义这里也需要获取 &String,而不是String
fn calculate_length(s: &String) -> usize {
s.len()
} // s 离开了作用域,但是因为它是一个引用,所以这里什么也不会做
引用允许我们使用值,但是不获取其所有权。
与引用相反还有一个解引用(dereferencing),使用解引用运算符 *。
借用的值,是不允许修改的。如果需要修改引用的值,那么我们就需要可变引用。
可变引用
fn main() {
// 使用可变引用必须是可变变量
let mut s = String::from("hello");
// 这里是可变引用
change(&mut s);
}
// 函数也要声明
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
- 可变引用有一个很大的限制:在同一个时间,只能有一个对某一特定数据的可变引用。
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 这里报错
println!("{}, {}", r1, r2);
防止同一时间对同一数据进行多个可变引用的限制允许可变性,但以一种受限的方式允许。
好处在于:可以在编译时,避免数据竞争。
数据竞争:
- 两个或者更多指针同时访问同一个数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
数据竞争会导致未定义行为,难以在运行时追踪,诊断以及修复。
在可变引用和不可变引用中存在类似规则:
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{}, {}, and {}", r1, r2, r3);Ï
不可以在拥有不可变引用的同事去拥有可变引用。
但是当引用的作用域结束以后,就可以重新使用了。比如:
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 没问题
println!("{}", r3);
编辑器在作用域结束之前判断不再使用的引用的能力被称为 "非词法作用域生命周期"。
悬垂引用
悬垂指针是指 某个指针指向的内存已经被分配给其他持有者。 在Rust中,编译器会确保引用永远不会变成悬垂状态。
小结,在Rust中:
- 在任一给定时间, 要么只能有一个可变引用,要么只能有多个不可变引用
- 引用必须总是有效的。
Slice 引用
- slice允许我们引用集合中一段连续的元素序列,而不用引用整个集合。
- slice是一种引用
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
它在堆中的指向类似于这样:
..是rust的range语法,语法类似:
let s = String::from("hello");
// 省略开头
let slice = &s[0..2];
let slice = &s[..2];
// 省略尾部
let slice = &s[3..len];
let slice = &s[3..];
slice获取的字符串, 在原始s被释放以后就不能被获取了。因为:
- RUST不允许同时存在一个可变引用和一个不可变引用。
比如这个例子:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // 错误!
println!("the first word is: {}", word);
}
这里在printLn中就会抛出一个编译错误
字符串字面值就是一个slice
从slice的角度来理解为什么字符串字面量:
let s = "Hello, world";
这里的s类型是&str:一个指向二进制程序特定位置的slice。
所以字符村字面量是不可变的,&str是一个不可变引用。