原文链接: https://www.warambil.com/variables-and-memory-management-in-rust
原文标题:Variables and memory management in Rust
公众号: Rust 碎碎念
最近,由 Mizilla 团队在 2010 年左右开发的 Rust 编程语言引起了很多争议。
事实是,这门作为 C++替代品而诞生的语言,随着它变得更加成熟,已经开始流行起来了,这主要是因为下面的关键特性:
- 安全性(Safety)
- 极好的性能(great performance)
- 打包和发布(Packaging and distribution)
- 给力的社区(Helpful community)
本文的目的是了解更多关于 Rust 是如何处理变量和内存从而在编译期而非运行期捕获错误。这也是使其成为关键应用选择的安全语言的特性之一。
Rust 使用了一个被称为linear types的概念。这意味着是基于线性逻辑(linear logic),确保对象只被使用一次,然后被安全的移除或释放。
Linear type 系统允许(使用)引用而不允许别名。在 Rust 中,变量默认是不可变的,因此,下面的代码会触发一个编译错误:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
如果你确实需要在变量被赋值之后对其进行修改,你必须以这种方式指定:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
现在,你一定想知道,为什么不可变性(immutability) 应该是一个 事实上的(de-facto) 特性,并且这种方式是如何有助于产生更安全的代码?
接下来你应该理解 Rust 在内存管理中的另一个概念:所有权(Ownership)。
理解所有权(Understanding Ownership)
Rust 没有像 Java 或者其他语言中带有的垃圾回收器。它的哲学理念是基于这种前提,即无论是谁声明了一个变量,它都应该拥有这个变量直至流程结束。但是在深入这个概念之前,我们需要重新认识一些关于内存的旧的概念:栈(stack) 和堆(heap) 。
尽管栈(stack) 和堆(heap) 都是可以在运行期使用的内存空间,但它们有所不同。栈(stack) 以一种后进先出(LIFO,last in, first out)的方式工作,这意味着它存储到来的值并以一种相反的顺序将值移出。
关于栈最重要的概念就是,它存储的所有数据必须是已知(译者注:这里指可在编译期确定大小)的固定大小。如果我们不知道我们要存储的数据有多大,那么我们应该使用堆。
堆的组织性不是很严谨,无论什么时候你需要在堆上放入数据,你都可以请求指定数量的空间,操作系统找到一个能够满足的空地方并返回一个指针,该指针持有这个空地方在内存中的地址。这个过程被称为分配(allocating) 。
把值放到栈上不算是分配,因为在放入之前数据的预留空间是已知的。因此,使用栈比在堆上分配更快,因为操作系统不需要去搜寻一个地方来存放数据。栈上的数据永远在栈顶。
当一个函数被调用时,传给函数的参数和函数的局部变量一样被推到栈上。当函数结束时,这些值从栈上被移除。
现在,那些允许开发者在堆上分配(空间,存放)数据(使用指针)的语言的主要问题,在于追踪哪部分代码正在使用堆上的哪些数据以及从堆上释放未使用的数据。这正是 Rust 的所有权(Ownership) 概念尝试去解决的问题。
Rust 中的每个值都有一个变量,该变量可以被看做它的所有者,并且每个值每次只能拥有一个所有者。无论何时所有者离开作用域,这个值也就没有了。
让我们来看下面的例子:
let s = String::from("hello");
这里我们使用一个变量,该变量指向内存中的一个 String 结构体。这个 string 结构体被分配在堆上(译者注:此处说法有误,应该是在栈上),并且s是一个拥有这个 String 结构体地址信息(即它的长度和容量)的变量。
现在,这个s默认是不可变的,所以如果我们想要修改这个 String 数据,我们应该这么做:
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This
mut关键字让 Rust 知道了这个变量的可变性。
现在,到了所有权比较有趣的部分,让我们看看下面的代码:
let s1 = String::from("hello");
let s2 = s1;
在这种情况下,s1被赋值给s2,因此,如果我们以像 C++这种其他语言的方式来思考的话,s2和s1都指向相同的结构体。因此,在同一个函数中赋值之后,我应该能够这样写:
println!("{}, world!", s1);
但是这行代码在 Rust 中是失败的,因为一旦s1被赋值给s2,s1就不再是数据的所有者了,所以它也就不再有效。这就是关于所有权的内容。通过这种方式,Rust 不必担心于追踪这种场景,即这个结构体数据通过s2被移除,而s1仍然指向这个数据(译者注: 即 C++中的悬垂指针)。
这里你可能会问,有没有办法进行深拷贝,通过一个副本,使s1和s2都指向一份有效的数据?答案是使用一个叫做clone的函数。
let s1 = String::from("hello");
let s2 = s1.clone();
目前为止,一切顺利。我们知道所有权在 Rust 中可以工作,但是在函数中呢?为了弄清这个问题,让我们看看下面的例子:
fn main() {
let s1 = String::from("hello");
let s2 = takes_ownership(s1);
println!("s1 '{}'.", s1); //not ok
println!("s2 '{}'.", s2); //ok
}
fn takes_ownership(st: String) -> String {
st
}
这里,s1作为参数,被传递给takes_ownership这个函数。不像在其他的语言中,函数会把参数拷贝一份到局部变量,Rust 仍然坚持它的规则,s1一旦被传入到takes_ownership这个函数,它就不再存在于main()函数的作用域了。
现在,如果我们需要借用(borrow) 所有权呢?接下来我们应该传递变量的一个引用,就像下面这样:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
这里我们传递s1的引用,s和s1就指向相同的结构体。这里的语义很重要,因为&s1让我们创建了s1的值的引用但是没有拥有它 。这是一个很重要的区别,因为s1的真正的值仅当真正的所有者离开main()作用域之后才会才会被释放。
&s1语法让我们创建一个引用,指向s1的值但没有拥有它。因为它没有拥有,所以当引用离开作用域的时候,它指向的值不会被释放。
我们不能在calculate_length()中修改这个 String 结构体,因为我们借用s1而没有拥有它。(译者注:此处说法不太严谨,不能修改是因为借用是不可变的,改为可变借用即可修改)
现在,如果我们需要修改这个被s1拥有的 String 并同时让保持其所有者为main()函数里的s1,应该怎么办呢?
对于这种情况,我们有个叫做可变引用( Mutable References)的东西。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
这种方式使用mut可以达到目的,但是只有一个警告,处于安全性考虑,对于一个特定的数据,在一个特定的作用域内,你只能使用一次可变引用。
因此,像下面这样的代码会报错:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
总结
任意数量的不可变引用或者一个可变引用可以在你的代码中的任意时刻存在。但是,引用必须总是有效的。
注: 本文的目的是找到一种方式来解释 Rust 是如何处理变量的,因此这里的大多数源码都是从官方的Rust 网[1]站文档里借用来的。
本文禁止转载,谢谢配合!! !欢迎关注我的微信公众号: Rust碎碎念
Rust网: https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html