文章目录
GoLang之Rust所有权
1.Rust所有权介绍
众所周知,Rust是一门以内存安全且高性能而著称的系统级编程语言。在Rust的世界里,没有GC,因此也没有内存回收时带来的STW问题,故而提高程序的运行效率;但是它也不需要我们手动管理内存而增加开发人员的负担。而在这内存安全和高性能的背后,得益于Rust的独有的所有权系统。
备注: 拥有GC(垃圾回收)语言的典型代表——Java, Go; 需要开发者手动管理内存的C/C++。
2.变量是分配在堆上还是分配在栈上?
在一些高级编程语言中,定义变量时我们很少关心这个变量是被分配在栈上还是分配在堆上,比如在Go, Python,几乎是不需要关心这个问题的。但是在Rust中不同,我们时时刻刻都要想着这个变量是分配在栈上还是在堆上,因为这会影响Rust的运行性能以及行为,甚至决定了我们的程序能否正常编译通过。
和栈是程序运行时可用的内存部分,他们最本质的区别在于数据组织结构不同。
栈是一个后进先出的组织结构,存储在栈上的数据大小是明确的,在程序编译期间就已经确定。我们说把变量分配在栈上,其实就是一个压栈的过程。
在编译期间无法确定占用内存大小的数据,或者数据大小是动态变化的,都需要分配在堆上。程序向操作系统申请在堆上分配内存时,会返回一个指针,因为指针的大小是固定的,因此指针会被存储在栈上面。当我们需要访问堆上的数据时,通过栈中的指针就能访问了。
备注:
入栈比在堆上分配内存更快——即写数据。因为入栈位置始终在栈的顶部,操作系统无需搜索可用空间;但是堆上分配内存时,操作系统是需要找出足够用的内存块的,因此相对较慢。
访问栈上的数据比访问堆上的数据更快——即读数据。因为我们必须先从栈上拿到指向堆的指针,才能继续访问堆上的数据(有这功夫栈上数据都已经返回了);此外,栈上的数据往往可以直接存储在CPU高速缓存中,而堆上的数据只能在内存里面待着。众所周知…高速缓存的访问速度比从内存中读取快多了。
3.堆和栈上的数据何时回收?
压入栈中的数据,会在栈返回时依次移除。如函数调用时传递的参数会被依次入栈,在函数结束时会反向移除。
但是堆上的数据是没有组织结构的,想要回收数据释放内存就麻烦了。前面提到过两种方法:GC和手动管理。
GC的弊端就是性能差,存在STW(Stop The World)问题。手动管理就更麻烦了,对程序员要求很高,需要我们一直关注内存的分配和释放,否则会出现很多内存安全问题,如悬空指针、多个引用同时释放导致的不确定行为等。
而今天的主角Rust,则是另辟蹊径,用一个全新方案——所有权系统,来解决内存管理问题。
4.Rust的所有权
所有权是Rust程序如何管理内存的一组编译规则,在编译期间由编译器负责检查并给出检查结果。
4.1所有权规则要牢记
1.Rust中的每一个值都有名为 owner(所有者) 的变量
2.Rust中的每一个值 有且只有 一个所有者
3.当所有者离开作用域范围时,这个值就会被释放(drop)
4.2作用域
Rust 作用域与其他编程语言类似,一个变量的作用域基本是在一个 {} 范围内有效,超过 } 这个范围就无效了。
fn main() {
let _s1 = "hello";
{
let _s2 = "world";
} // 变量 _s2 在这里被释放
// 取消s2的输出会报错:cannot find value `s2` in this scope
// println!("{}", s2);
println!("{}", s1);
} // 变量 _s1 在此处被释放
4.3 变量与数据的交互方式
Rust 之所以能做到每个值只有一个所有者,是因为其所有权转移机制。在Rust中,变量和数据的交互有两种方式:move 和 clone。
所有权转移: move
对于分配在堆内存上的变量,当将其赋值给另一个变量时,就会发生所有权的转移。所有权转移时, 其实是将拷贝了值在栈中的指针地址、长度和容量,并没有赋值实际的数据。 如下示例,s1 分配在堆上, 将其赋值给s2后,所有权就转移到s2了,当再次访问s1时编译器就会提示 s1已经被move掉了。
fn main() {
// String 是字符串类型,分配在堆上
let s1 = String::from("hello");
println!("{}", s1);
let s2 = s1;
println!("{}", s2);
// 注释掉对 s1 的访问,程序就能正常编译了
println!("{}", s1); // borrow of moved value: `s1`
}
如果有其他语言的使用经历(比如Python)就会发现,Rust中的move机制,与对象的浅拷贝类似。但是在Rust中,被"拷贝"的原变量会被删除,因此称之为move是在再合适不过了。
数据拷贝:clone
上面的move中的示例代码,如果需要让他编译通过,只需要做稍许改动即可,如下:
fn main() {
let s1 = String::from("hello");
println!("{}", s1);
// 注意此处。使用字符串的 clone方法进行变量的赋值
let s2 = s1.clone();
println!("{}", s2);
println!("{}", s1);
}
这里的 s2 = s1.clone(); 是没有所有权转移的,因为clone方法将s1指向的数据完整的赋值一份并由s2接管。因此在内存中, 此时是会有两个 “hello” 字符串。clone时,同样会有堆内存的分配过程,因此 clone相对move来说,是逻辑较重的行为,性能也自然更低了。
对此,Rust的设计是:对于分配在堆内存上的数据,Rust进行赋值时默认都是使用 move。
栈数据拷贝
fn main() {
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
}
根据前面的经验,这个示例应该会报所有权的错误。而实际上这段代码是能正常编译的…
前面说过,对于编译时大小是固定的数据,我们是能把数据分配在栈内存上的,没必要放到堆上。而栈上的数据拷贝是非常快的,因此Rust实现中,对于编译时能知道大小的数据,直接放栈内存上即可,栈上数据拷贝也是拷贝一份完整数据。因此栈上的数据是没有深拷贝和浅拷贝这种说法,换句话说就是没有move和clone这种概念。
Rust 有一个特殊的注解,称为Copy Trait,我们可以将它放在像整数一样存储在堆栈中的类型上。如果一个类型实现了Copy Trait,一个变量在赋值给另一个变量后仍然有效。
对于何种类型可以Copy,您可以检查给定类型的文档确定。作为一般规则:
任何一组简单的基本类型都可以实现Copy;没有任何需要分配或某种形式的资源可以实现Copy。以下是一些实现的类型Copy:
1.所有整数类型,例如u32
2.布尔类型
3.所有浮点类型,例如f64
4.字符类型,char
元组——元组中的元素也需要实现Copy 特征。 例如,(i32, i32)实现了Copy,但(i32, String)没有。
4.4函数与所有权
将变量传递给函数时一样会有 移动 和 复制 的行为。下面示例展示了函数调用时传递栈上的数据和堆上的数据会有什么不同。
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s指向的值发生所有权转移,被move进函数
// println!("s: {}", s); // 尝试打印x会报错:borrow of moved value: `s`
let x = 5; // x进入作用域
makes_copy(x); // x是i32类型,实现了Copy特征,
// 值被拷贝一份传给函数
println!("x: {}", x)
} // x 超出作用域;然后是s。但是因为s的值已经被move走,因此不会有清理动作
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 超出作用域
这里需要注意的是:s的值的所有权被move进takes_ownership函数并赋值给some_string。takes_ownership 函数结束时,some_string会被drop掉。但是当main函数返回时,由于 s已经失效了,因此不会有针对s的drop动作。
类似的,函数的返回值也有所有权,如:
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值移给 s1
let s2 = String::from("hello"); // s2 进入作用域
// s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
let s3 = takes_and_gives_back(s2);
} // 这里, s3 移出作用域并被drop。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被drop
// gives_ownership 将返回值移动给调用它的函数
fn gives_ownership() -> String {
// some_string 进入作用域.
let some_string = String::from("hello");
// 返回 some_string 并move给调用方
some_string
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
可以看到,如果需要在函数间传值、返回值时,会一直涉及到所有权调用,这代码写起来就折腾人了,真的让人头大。那Rust有没有更好的一个方法呢?且听下回分解…
5.总结
Rust创造性的引入了所有权来解决由于程序员的不靠谱而导致的内存安全相关问题,把人为导致的不安全因素在编译器就全部暴露出来,确实是奥利给!而且还抛弃了GC,简直是6的不行。但同样的,设计的复杂性也带来了使用的复杂性,所有权系统也是很难掌握的,需要不断的被编译器教育才行…Rust出了名的难学(难到用Rust实现一个链表都能出一本书),所有权系统当记一功!