Rust竟然没有垃圾回收
Rust没有垃圾回收,靠的是所有权和相关工具实现的内存安全。Rust使用包含特定规则的所有权来管理内存,这套规则允许编译器在编译过程中检查,不会产生运行时开销。
栈与堆
栈:
保存在栈里的数据拥有固定的大小(因为数据的类型固定)。指针大小是固定的,在编译器可以确定,因此可以将指针存放在栈中。
堆:
编译期无法确定大小的数据存放在堆中,堆空间的管理较为松散。
请求堆空间,操作系统会在堆内存中找到一块足够大小的空间,将它标记为已使用,并把指向该内存地址的引用返回,堆分配称为分配。
小结:
1.栈的分配比堆分配更有效率,栈的分配直接压入栈顶;堆的分配需要寻找一块合适大小的内存。
2.数据访问,栈比堆的查找也更快。指令在内存中跳转的次数越多,性能越差。
所有权规则:
1. Rust中的每一个值都有一个对应的变量作为它的所有者
2. 在同一时间内,值有且仅有一个所有者
3. 当所有者离开自己的作用域,它持有的值就会被释放掉。Rust在变量离开作用域时,会调用一个叫作drop的特殊函数
移动(move:堆):
移动(move)的本质是为了解决多个变量的指针指向同一个堆内存地址,避免多个变量退出作用域的时候,多次释放内存,造成安全问题。
let s1 = String::from("hello");
let s2 = s1;
println!("{}",s1);
println!("{}",s2);
克隆(clone:堆):
克隆会在堆内存上复制一份数据,同样变量s2在栈上也会存在新的指针。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1={},s2={}",s1,s2);
栈上数据的clone:
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
基本数据类型,内存大小固定,会存储在栈上,y = x,会默认发生clone,而不是移动(move)。
Drop、Copy trait:
和clone、move相关的两个Trait。实现了Drop trait,在变量离开作用域的时候会自动调用drop方法。实现了Copy trait,变量可以调用clone方法来在堆内存中深拷贝自己的数据。
实现了Drop trait的类型不能再实现Copy trait。
实现了Copy Trait的类型:
任何简单标量的组合类型都可以是Copy的,任何需要分配内存或某种资源的类型都不会是Copy的。
• 所有的整数类型,诸如u32。
• 仅拥有两种值(true和false)的布尔类型:bool。
• 字符类型:char。
• 所有的浮点类型,诸如f64。• 如果元组包含的所有字段的类型都是Copy的,那么这个元组也是
Copy的。例如,(i32, i32)是Copy的,但(i32, String)则不是。
函数与所有权:
将值传给函数,类似于对变量进行赋值。将变量传递给函数,会发生移动或复制。
堆数据类型:传给函数的时候会发生移动(move), 原来的变量不再拥有所有权。
栈数据类型:传给函数的时候会发生复制(copy), 原来的变量仍然拥有原来数据的所有权。
let s = String::from("hello");
takes_overship(s);
// println!("{}",s);
let s = 5;
makes_copy(s);
println!("{}",s);
fn takes_overship(some_string:String){
println!("{}",some_string);
}
fn makes_copy(some_integer:i32){
println!("{}",some_integer);
}
函数返回值与所有权:
函数在返回值的过程中也会发生所有权的转移。
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
fn gives_ownership() -> String{
let some_string = String::from("hello");
some_string
}
fn takes_and_gives_back(a_string:String)->String{
a_string
}
小结:变量所有权的转移总是遵循相同的模式:将一个堆上的值赋值给另一个变量时就会转移所有权。当一个持有堆数据的变量离开作用域时,它的数据就会被drop清理回收,除非这些数据的所有权移动到了另一个变量上。
引用(&)和借用(borrow):
&代表的就是引用语义,它们允许你在不获取所有权的前提下使用值。(与使用&进行引用相反的操作被称为解引用(dereferencing),它使用*作为运算符。)
(代码和图片来自《Rust编程指南》)
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("{},{}",s1,len);
fn calculate_length(s:&String) ->usize{
s.len()
}
本质上,&s1语法在不转移所有权的前提下,创建一个指向s1值的引用(指针)。
函数签名中的&用来表明参数s的类型是一个引用。
变量s的有效作用域与其他任何函数参数一样,唯一不同的是,它不会在离开自己的作用域时销其指向的数据,因为它并不拥有该数据的所有权。
这种通过引用传递参数给函数的方法也被称为借用(borrowing)(借了东西,总是要归还的)
可变引用:
引用默认是不可变的,但是可以通过&mut String,让引用变为可变。
let mut s = String::from("hello");
change(&mut s);
fn change(some_string:&mut String){
some_string.push_str(", world!");
}
可变引用的限制:一次只能声明一个可变引用。这种限制可以帮助我们避免数据竞争。数据竞争:
• 两个或两个以上的指针同时访问同一空间。
• 其中至少有一个指针会向空间中写入数据。
• 没有同步数据访问的机制。
数据竞争会导致未定义的行为,由于这些未定义的行为往往难以在运行时进行跟踪,也就使得出现的bug更加难以被诊断和修复。
引用与不可变性:
1. 不能在拥有不可变引用的同时创建可变引用。(下不可变引用的用户可不会希望他们眼皮底下的值突然发生变化!)
3.不过,同时存在多个不可变引用是合理合法的
悬垂指针:
概念:指针指向曾经存在的某处内存地址,但该内存已经被释放掉甚至是被重新分配另作他用了。
在Rust语言中,编译器会确保引用永远不会进入这种悬垂状态。假如我们当前持有某个数据的引
用,那么编译器可以保证这个数据不会在引用被销毁前离开自己的作用域。
{
let reference_to_nothing = dangle();
}
fn dangle() -> &String{
let s = String::from("hello");
&s
}
变量s在这里离开作用域并随之被销毁,它指向的内存自然也不再有效。
引用总结:
• 在任何一段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
• 引用总是有效的。
切片 (slice):
不持有所有权的数据类型。切片允许我们引用集合中某一段连续的元素序列,而不是整
个集合。
字符串切片:
字符串切片是指向String对象中某个连续部分的引用
let s = String::from("hello world!");
let hello = &s[0..5];
let world = &s[6..11];
切片引用的本质也是在栈中创建指针、len和capacity,指向堆内存。
字符串字面量就是切片
let s = "Hello, world!";
变量s的类型其实就是&str:它是一个指向二进制程序特定位置的切片。正是由于&str是一个不可变的引用,所以字符串字面量自然才是不可变的
fn first_word(s: &str) -> &str {
有经验的Rust开发者往往会采用下面的写法,这种改进后的签名使函数可以同时处理String与&str:你持有字符串切片时,你可以直接调用这个函数。而当你持有String时,你可以创建一个完整String的切片来作为参数。
其他类型切片:
1. 数组切片:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
这里的切片类型是&[i32],它在内部存储了一个指向起始元素的引用及长度,这与字符串切片的工作机制完全一样
总结:
所有权、借用和切片的概念是Rust可以在编译时保证内存安全的关键所在。