Rust语言的核心目标就是解决内存回收的问题,因为目前的内存回收都有其不同的缺点。C/C++语言中,开发人员需要手动申请和释放内存,这就需要开发人员编写代码的时候小心谨慎地管理内存。带有垃圾回收器的语言,如C#、Java、Golang等,虽然使得开发人员不再担心内存回收的问题,却降低了程序的性能。
Rust语言通过一些规则解决了这个问题,规则的核心就是对值的所有权的约定。
- Rust 中的每一个值都有一个所有者(owner)。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
这里的值可以理解为对应的一块内存空间,每一个分配的内存空间在任一时刻都有且仅有一个所有者,当所有者离开作用域的时候,这块内存就会被释放。
既然内存是在所有者离开作用域时释放的,那么我们首先确定什么是作用域。像其他语言一样,Rust使用一对花括号{}来表明作用域。
// x和s都会在离开方法的时候被释放 fn main() { let x = 5; let s = String::from("Hello World"); { let y = 1; // y在离开第二个花括号的时候释放 } }
当然也有特殊的情况,如果一个变量在离开作用域之前就不再使用,就会被任务提前结束了生命周期,这种能力被称之为非词法作用域生命周期。
fn main() { let mut s = String::from("hello"); let r1 = &s; // 没问题 let r2 = &s; // 没问题 println!("{} and {}", r1, r2); // 此位置之后 r1 和 r2 不再使用 let r3 = &mut s; // 没问题 println!("{}", r3); }
在确定作用域后,下面最重要的就是对值的所有者进行确权。
一般情况下,在定义变量的时候,值得所有者是变量本身。
fn main() { // x和y分别拥有各自值的所有权 let x = 5; let y = String::from("hello") }
将变量的值赋给一个新的变量,对于值类型和引用类型的处理方式是不同的。因为值类型是拷贝复制,所以新的变量和原有的变量拥有各自值的所有权。引用类型则是创建了一个指向值的指针,所以值的所有权就被转移(move)到新的变量身上。这时候旧的变量就会被丢弃,在此后的代码中就不能再访问了。
fn main() { // 对于值类型,x和y分别拥有各自值的所有权 let x = 5; let y = x; // 对于引用类型,b拥有了值的所有权,a被丢弃 let a = String::from("hello"); let b = a; // 此处编译会报错,因为a的值已经move到b的身上 println!("{} world!", a); }
方法传参在值类型和引用类型上的表现与赋值是一致的。当值类型作为参数的时候,方法内部会生成值的拷贝,并在退出方法的时候释放。而传递引用类型的时候,值的所有权会转移到方法内部,并在退出方法的时候释放。这样在调用方法之后,就不能再访问原来的引用变量了。
fn main() { let x = 5; let y = String::from("hello"); say_hello(y); print_number(x); // 此处可以正常打印 println!("{}", x); // 此处会有编译错误,因为y已被丢弃 println!("{} world!", y); } fn say_hello(s: String) { println!("{} world!", s); } fn print_number(x: i32) { println!("{}", x); }
当调用方法之后获取返回值,返回值会将所有权转移到新的变量上。
fn main() { // gives_ownership 将返回值转移给 s1 let s1 = gives_ownership(); // s2 进入作用域 let s2 = String::from("hello"); // s2 被移动到takes_and_gives_back 中, 它也将返回值移给 s3 let s3 = takes_and_gives_back(s2); } // gives_ownership 会将返回值移动给调用它的函数 fn gives_ownership() -> String { 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 }
对于引用类型,在进行传参的时候值的所有权会转移到方法内容。如果还要继续使用它只能定义一个新的变量接收方法的返回值,这样就太麻烦了,Rust提供了引用的功能来解决这个问题。同其他语言一样,Rust使用 & 符号代表引用变量的地址。
fn main() { let x = 5; // 定义变量的引用 let y = &x; let s1 = String::from("hello"); say_hello(&s1); // 此处可以编译通过,因为调用方法的时候传入的是s1的引用 println!("{} world", s1); println!("{}", y); } fn say_hello(s: &String) { println!("{} world", s) }
通过上面的方法,可以在不定义新变量的情况下,继续使用原有变量。但是在方法内部是不能对传入的值进行改变的,因为所有权并没有发生转移,值“hello”的所有权还是属于s1。s只是借用了s1的值。
fn main() { let s1 = String::from("hello"); say_hello(&s1); } fn say_hello(s: &String) { // 此处会报错,因为s1在这里不可以被变更 s.push_str(" world"); println!("{} world", s); }
实际上如果参数没有被声明为可变类型,任何类型的传值都不允许在方法内部进行变更。
fn main() { let s1 = String::from("hello"); say_hello(s1); } fn say_hello(s: String) { // 此处会报错,因为s不是可变类型 s.push_str(" world"); println!("{} world", s); }
如果想要更改引用的值,参数必须声明为可变类型。
fn main() { let mut s1 = String::from("hello"); say_hello(&mut s1); } fn say_hello(s: &mut String) { // 正常运行 s.push_str(" world"); println!("{} world", s); }
这里可以看到,想要在方法内部更改引用的值必须满足变量s1、调用时传参的声明、方法形参的声明都是可变类型的。
引用是借用变量的值,而可变引用则是暂时拥有了值的所有权。根据核心原则第二条值在任一时刻有且只有一个所有者下面的语句明显是会报错的。
fn main() { let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2); }
r1在获得了值的所有权后,r2又去申请所有权,这样就造成了数据的竞争。数据竞争会导致不可预料的变更,一般有以下三个行为造成:
Rust不允许这样的行为存在,所以在编译阶段就避免了它。
游离指针是指指针指向的值已被释放,但是指针本身并没有。这种情况Rust在编译的时候也会进行检测。
fn main() { let s1 = dangling(); } fn dangling(): &String { let s = String::from("hello"); &s // s在退出作用域的时候会被释放,所以在外部接收会得到一个游离的指针。 // 这里编译的时候会报错 }
本文作者:谭三皮
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!