深入理解Rust的所有权和借用
文章目录
- 所有权
- 所有权基本规则
- 值的拷贝
- 传递函数
- 借用
- 借用分类
- 借用的原则
- 总结
Rust编程语言的所有权机制和借用是它的核心特性之一,旨在确保内存安全、并发安全以及避免数据竞争。由于所有权机制,不需要通过垃圾回收进行内存处理,在保证高性能的同时,还保证了内存安全。通过该机制Rust在编译时就能检查程序的内存安全问题,而不需要在运行时进行额外的开销。理解Rust的所有权和借用是掌握Rust编程的关键。下面详细介绍一下这两个概念
所有权
所有权是Rust的一项独特的内存管理机制,它在编译时就能强制确保内存的安全使用。Rust通过所有权来控制每个值的生命周期和访问权限。每个值在Rust中都有一个"所有者",并且每个值只能有一个所有者。这样做的目的就是避免内存泄漏、数据竞争和悬挂指针等问题。
所有权基本规则
1.每个值都有一个所有者: 在Rust中 每个数据都由一个变量(所有者)拥有。变量是数据的所有者。
2.值在同一时间只能有一个所有者: 当一个值的所有者被转移时,原所有者将不再能够使用该值,Rust会确保在转移后不会有两个所有者持有该值。
3.所有权一旦转移,原所有者将不再有效: 一旦变量的所有权转移,原变量将无法再访问这个值。这种机制叫做所有权转移。
4.当离开变量对应的作用域时,变量这个值将被丢弃。
fn main() {
let s1 = String::from("Hello");
//s1的所有权被转移到 s2
let s2 = s1;
//println!("{}", s1); //error: s1的所有权已被转移,编译报错
println!("{}", s2); //right: s2拥有s1原来的值
//通过这种方式避免了指针悬挂
{
let s3 = String::from("Inner Rust");
println!("{}", s3); //right 这里能访问到
}
//离开了变量的作用域 变量值被丢弃 访问异常
println!("{}", s3); //error 访问不到
}
值的拷贝
基本类型的变量比如i32、i64、u32、u64等固定大小的简单值,通过自动拷贝来进行赋值都被存在栈中,完全无需在堆上分配内存。所以不需要发生所有权的转移。在堆上分配内存的值,在赋值时才需要发生所有权的转移。String类型是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成,其中堆指针指向了真实存储字符串内容的堆内存,所以在赋值的时候会发生所有权的转移。
进一步,任何基本类型的组合可以自动拷贝的,不需要分配内存或某种形式资源的类型是可以自动拷贝的。
- 所有整数类型布尔类型、浮点类型、字符类型(char)
- 元组,当且仅当其包含的类型也都是Copy的时候。比如(i32, i32)是Copy的,但(i32, String)不是
- 不可变引用&T ,但是注意可变引用&mut T是不可以Copy的
let x = 5;
let y = x; //x的值被拷贝到了y中
//这里x和y都有效
let s1 = String::from("hello");
let s2 = s1;
//所有权转移,s2有效s1无效
Rust不会自动创建数据的"深拷贝",因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone的方法。clone方法会生成一个全新的变量,堆上的内存也会被重新分配一份。
let s1 = String::from("hello");
let s2 = s1.clone();
//两个变量都有效
println!("s1 = {}, s2 = {}", s1, s2);
传递函数
将值传递给函数,一样会发生所有权移动或者复制,就跟let语句一样,下面的代码展示了所有权、作用域的规则。
fn takes_ownership(some_string: String) {
println!("{}", some_string);
//执行完毕之后 外部传入的string会被drop释放掉
}
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
//基本类型不会 drop 在栈上被处理
}
fn main() {
let s1 = String::from("hello rust");
//s1的所有权被转移到函数中
takes_ownership(s1);
//这里访问s1会出问题
let x = 5;
//x是基本类型 没有在堆上分配内存 是Copy的
makes_copy(x);
//这里x可以继续使用
}
通过函数的返回值转移所有权
//在函数内部分配内存空间
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 = gives_ownership();
let s2 = String::from("hello");
//s2的所有权转移到函数内部 然后通过返回值给到s3
let s3 = takes_and_gives_back(s2);
}
借用
借用允许你访问一个值而不取得其所有权。Rust通过借用来确保不会违反所有权的规则,同时使得多次读取和使用同一数据变得安全。
借用分类
Rust中的借用分为两种类型:
1.可变借用(Mutable Borrow)
可变借用允许你通过借用访问并修改数据。在同一时间,只有一个可变借用者能够访问数据。
2.不可变借用(Immutable Borrow)
不可变借用允许多个借用者同时读取数据,但在同一时刻不能有可变借用。也就是说Rust不允许同时拥有不可变借用和可变借用。
//同一作用域,特定数据只能有一个可变引用
fn main() {
let mut s = String::from("Hello");
//可变借用
let s2 = &mut s;
//let s3 = &mut s; //error
s2.push_str(", World!");
println!("{}", s2);
}
//允许多个不可变借用存在
fn main() {
let s = String::from("Hello");
let s1 = &s; // 不可变借用
let s2 = &s; // 另一个不可变借用
println!("{}", s1); // 正确: 输出 "Hello"
println!("{}", s2); // 正确: 输出 "Hello"
}
//可变引用与不可变引用不能同时存在
fn main() {
let s = String::from("Hello");
let s1 = &s; // 不可变借用
let s2 = &mut s; // 可变借用 报错
}
借用的原则
1.同一作用域,特定数据只能有一个可变引用;这种限制的好处就是使Rust在编译期就避免数据竞争
2.可变引用与不可变引用不能同时存在,允许多个不可变借用存在;
多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。
总结
Rust 的所有权和借用机制通过以下几点确保内存安全:
1.每个值只能有一个所有者,且所有者会自动负责清理内存。
2.可以通过不可变借用或可变借用来访问数据,但必须遵循并发借用规则。
3.借用的生命周期与引用的有效性密切相关,编译器通过生命周期分析来确保引用不会悬挂。
通过这些特性,Rust 提供了强大的内存安全性和并发安全性,使得开发者能够避免许多常见的内存管理错误,同时不依赖垃圾回收。