基本原则:
- Rust 中的每个值都有一个变量,称为其所有者。
- 一次只能有一个所有者。
- 当所有者不在程序运行范围时,该值将被删除。
变量与数据交互的方式
变量与数据交互的方式:移动(move)和克隆(clone)两种:
如果我们定义了一个变量并给它赋予一个值,这个变量的值存在于内存中;
如果我们需要储存的数据长度不确定(比如用户输入的一串字符串),我们就无法在定义时明确数据长度,也就无法在编译阶段令程序分配固定长度的内存空间供数据储存使用, 这就需要提供一种在程序运行时程序自己申请使用内存的机制——堆。
移动
- 如果数据是“基本数据”类型的数据,不需要存储到堆中,仅在栈中的数据“移动”方式是直接复制。基本数据类型包括:整型(i32,u32,i64等), 浮点类型, 布尔类型,字符类型(char),包含以上类型的元组(tuple)。
- 如果发生交互的数据在堆中,例如字符串,则新的String对象s2只是将指针指向堆中被复制的字符串s1,只有栈中的数据被复制了,堆中的字符串依然是原来的字符串。
当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存,因此,如果s1与s2都被释放,则堆中的数据被释放两次,这是不允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了,不可以再使用。
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 错误!s1 已经失效
克隆
Rust会尽可能地降低程序的运行成本,所以默认情况下,长度较大的数据存放在堆中,且采用移动的方式进行数据交互。但如果需要将数据单纯的复制一份以供他用,可以使用数据的第二种交互方式——克隆。例如:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
涉及函数的所有权机制
函数输入传值
将一个变量当作函数的参数传给其他函数,那么它和移动的效果是一样的:基本数据类型被传入函数后,变量依然有效,但可变长数据类型被传入后,变量失效,不能再使用。
函数返回值
被当作函数返回值的变量所有权将会被移动出函数并返回到调用函数的地方,而不会直接被无效释放。
引用与租用
引用是变量的间接访问方式,”&“运算符可以取变量的”引用“。当一个变量的值被引用时,变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值。
- 引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置。
- 引用不会获得值的所有权,因此,函数调用后,变量依然有效。如果要改变引用的数据,需要使用
&mut
修饰可变的引用类型。 - 可变引用与不可变引用相比除了权限不同以外,可变引用不允许多重引用,但不可变引用可以。由于发生数据访问碰撞的必要条件之一是数据被至少一个使用者写且同时被至少一个其他使用者读或写,所以在一个值被可变引用时不允许再次被任何引用。
- 引用只能租借(Borrow)值的所有权,不能修改所有者的值。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
let mut s2 = &s1; // s1的所有权被租借给s2, 不能修改
let s3 = s1; // s1的所有权被移动到s3
println!("{}", s2); // 错误,s2不再具有s1的所有权,成为无效的
let mut s4 = String::from("world");
s2 = &mut s4;
s2.push_str("!");
println!("{}", s2);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
垂悬引用(Dangling References)
在有指针概念的编程语言里它就指的是那种没有实际指向一个真正能访问的数据的指针(注意,不一定是空指针,还有可能是已经释放的资源)。它们就像失去悬挂物体的绳子,所以叫"垂悬引用"。例如把一个函数内的变量的引用作为返回值返回(函数结束,变量被释放,引用为空),会造成这种情况。
引用生命周期
Rust 中的每一个引用都有其 生命周期(lifetime),也就是引用保持有效的作用域。Rust 编译器有一个 借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的:
- 被引用的对象比它的引用者存在的时间更短,程序被拒绝编译。
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
- 被引用的数据比引用者有着更长的生命周期,是一个有效的引用。
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
结构体数据的所有权
想要结构体拥有它所有的数据,需要整个结构的数据都是有效的,例如全部使用非引用类型。如果结构体存储其他对象拥有数据的引用,需要加上生命周期,否则将是无效的。
- 在结构体方法实现时,如果方法只是为了获取结构体中的数据,而不是写入,那么第一个方法参数通常使用
&self
来代替rectangle: &Rectangle
,而不是self
。 - 如果想要方法中改变调用方法的实例,需要将第一个参数改为
&mut self
。 - 仅仅使用 self 作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 self 转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。