所有权
什么是所有权?字面意思就是,这个这个数据是属于谁的。 比如 let a = 1213
那么 数值 1213 的所有权就属于变量a 。 在rust中,所有权有几个特点:
- 一个值只能有一个所有者,或者从内存的角度来讲,每一块内存只能拥有一个所有者。
- 当所有者离开作用时,这个值会被销毁,这片内存会被释放。
作用域
rust的作用域概念并不是JS中的作用域,而是与ES6的块作用域相似,有着非常明确的作用域界限,而不是像JS一样会声明提前。
css
// 这是一个作用域a
let a = 3;
{
//这是一个新的作用域b
let b = 3;
} //b的作用域结束,此时b不能再使用,同时数值3也被销毁
前面的章节,介绍了基础类型的变量,基础类型的变量与借用的关系不大。复合类型则是所有权和借用的核心。 我们以一个简单的复合类型来举例String类型。 需要注意,在rust中,String类型并不是字符串
rust
let a ='hello world' //这是一个字符串,它在rust中表示的类型是&str
let b = String::from("hello"); //这是一个String类型的值,它是一个复合对象
这点有点像TS中的string和String的区别。
转移所有权
我们先用一个基础类型来解释所有权的转移:
ini
let a = 5;
let b = a;
这段代码将 5
绑定到变量 a
; 接着拷贝 a
的值赋给 b
,最终 a
和 b
都等于 5
。 此时内存中存在了两个5
,他们的所有者分别是a和b,这样依然满足我们说的 '每个值都只有一个所有者'。 有人可能会疑问,这样是否消耗性能。当然不,因为他们都在栈上,只有4个字节。如果不复制实际的数字5,而去复制a的指针,所耗费的性能是一样的。
然后再来看一段代码:
ini
let a = String::from("hello world");
let b = a;
这是一个类型的代码,发生的事情也是类似的,只不过String是复合类型的数值,是放在堆内存的。 而变量a,对应的数值实际上是String类型实例存放的指针。 而再把a 赋值给b的时候,会把这个指针拷贝给了b。 此时,栈内存中存在了两个一样的指针,他们的所有者分别是a和b,这与上一个例子是一致的。 但是,问题是这两个指针指向的是同一片堆内存,这就有问题了,这意味着这一片内存同时拥有了两个所有者。 此时,就不满足'一个数值只有一个所有者'的要求了,于是这个过程中发生了所有权转移,这片内存的所有权,从a转交到了b,而a立马被遗弃了
css
let a = String::from("hello world");
let b = a;
//此时使用a,会报错
println!("{}", a);
那么,如果一个值允许两个所有者,会发生什么呢?
当变量离开作用域后,Rust 会自动调用 drop
函数并清理变量的堆内存。 但是两个 String
变量指向了同一位置,这就有了一个问题:当 a
和 b
离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。 因此,Rust 这样解决问题:当 s1
赋予 s2
后,Rust 认为 s1
不再有效,因此也无需在 s1
离开作用域后 drop
任何东西,这就是把所有权从 s1
转移给了 s2
,s1
在被赋予 s2
后就马上失效了。
再来看一段代码:
ini
fn main() {
let x: &str = "hello, world";
let y = x;
println!("{},{}",x,y);
}
如果参考之前的 String
所有权转移的例子,这段代码应该是有问题的。但实际却是正常的。 这就是之前说的,&str和String是两种不同的类型。"hellow world"本身是存放在数据段的,是常量,不可被更改,在进程结束之前也不会被释放。 Rust认为,对于&str类型,只是引用,不属于所有权,而且因为你无法直接对&str进行写操作,因此对它的引用是安全的。
以上String例子中的转移所有权问题,如果我们确实需要两个变量来表示同一份数据,可以使用clone。
css
let a = String::from("hello");
let b = a.clone();
println!("a = {}, b = {}", a, b);
这段代码能够正常运行,因为发生了深拷贝,a和b指向了不用的堆内存空间,当然这也会造成性能的下降。
函数参数和返回值
好了,下面到了最不容易察觉的所有权转移场景了。
在调用函数时,基础类型的参数会被深拷贝,而复合类型的参数只会浅拷贝,这个跟JS是一致的。 我们以一段JS代码为例:
css
// 一个函数,有两个参数。参数a为number,参数b为object
const change= (tmp1,tmp2)=>{
tmp1=a+1;
tmp2.name='hellow JS '
}
const a = 2;
const b={
name:'hello Rust'
}
change(a,b)
console.log(a,b.name) // 打印a是2, b.name是'hellow JS'
我们解释一下背后发生的事情, 当调用函数时,a的值被深拷贝了,并赋值给了一个临时变量tmp1,此时对tmp1的修改,不会影响到a。因此,a还是2。 b也被拷贝了,只是浅拷贝,拷贝了b对应的指针,并把这个指针赋值给了临时变量tmp2.但是由于tmp2和b指向了同一片内存,所以修改了tmp2也就修改了b。 这个过程中,发生了拷贝和赋值,自然也就产生了所有权的转移。所以在rust中这样执行:
css
fn main() {
let a = 2;
let b = String::from("hello");
change(a,b)// b发生了作用域转移,当change函数调用结束后,b对应的值就被释放了
println!("{},{}",a,b); //这里打印,a是正常的,b就会报错
}
同样的,函数返回值也有所有权,例如:
css
fn change(a:String){
....//对a进行修改
a //这是rust的返回语法,不用写return,仅仅写你想返回的值,并且不加;号即可
}
fn main() {
let a = String::from("hello");
let b = change(a);// a发生了作用域转移,但是所有权又被转移给了b
println!("{},{}",a); //这里打印,b是正常的,a就会报错
}
所有权避免了一部分内存安全问题,但是显而易见的带来了问题:我们需要将一个值来来回回的传递。为了解决这个问题,rust引入了另一个设计
引用和借用
常规引用是一个指针类型,指向了对象存储的内存地址。在下面代码中,我们创建一个 i32
值的引用 y
,然后使用解引用运算符来解出 y
所使用的值:
ini
fn main() {
let x = 5;
let y = &x;// 获取存储5的内存的指针
assert_eq!(5, x);
assert_eq!(5, *y);
}
变量 x
存放了一个 i32
值 5
。 y
是 x
的一个引用。可以断言 x
等于 5
。 然而,如果希望对 y
的值做出断言,必须使用 *y
来解出引用所指向的值(也就是解引用 )。 一旦解引用了 y
,就可以访问 y
所指向的整型值并可以与 5
做比较。
相反如果尝试编写 assert_eq!(5, y);
就就会直接报错,因为它们是不同的类型。
不可变引用
下面的代码,我们用 a
的引用作为参数传递给 change
函数:
rust
fn main() {
let a = String::from("hello");
change(&a);
println!("{}",a); //这里打印,a是正常的
}
fn change(s: &String) -> {
...//do something
}
需要注意两点不同:
- 传入的是引用,而不是直接传入了a,同时也不用再将a返回回去
change
的参数s
类型从String
变为&String
通过 &a
语法,我们创建了一个指向 a
的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。 于是我们开开信息的想修改借用的变量:
javascript
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
很不幸,发现报错了
javascript
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
------- 帮助:考虑将该参数类型修改为可变的引用: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
`some_string`是一个`&`类型的引用,因此它指向的数据无法进行修改
正如变量默认不可变一样,引用指向的值默认也是不可变的。
可变引用
只需要一个小调整,即可修复上面代码的错误:
rust
fn main() {
let mut a = String::from("hello");
change(&mut a);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
需要注意这几行代码中mut
的位置。 对于可变引用,rust有两个重要的限制:
1. 可变引用同时只能存在一个
不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制: 同一作用域,特定数据只能有一个可变引用:
ini
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
以上代码会报错:
go
同一时间无法对 `s` 进行两次可变借用
这种限制非常费解,但好处就是使 Rust 在编译期就避免数据竞争。如果同时存在多个函数或者线程对同一块内存进行写操作,就难免出现未知的问题,且这种问题难以诊断。rust通过这种方式,来杜绝这种问题,这更像是一种编程范式或者思想了。 这种时候,我们可以手动新增一个作用域:
ini
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;
2.可变引用与不可变引用不能同时存在
ini
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{}, {}, and {}", r1, r2, r3);
错误如下:
swift
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
// 无法借用可变 `s` 因为它已经被借用了不可变
这种做法自然也是为了数据安全,这样不会存在你读的数据被别人改了的情况。
但是,这里需要注意引用的作用域
注意,引用的作用域
s
从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号}
ini
fn main() {
let mut s = String::from("hello");
let mut b =3;
let r1 = &s;
println!("{} and {}", r1);
//r1作用域在这里结束
let r3 = &mut s;// 因为r1被释放了,所以这里可以再次赋值
b+=1;
}//b的作用域直到这里才结束
rust这一套所有权和借用的设计,很好的避免了以下问题:
- 内存未及时释放引起的泄漏
- 数据竞争
- 悬垂指针,二次释放等常见的指针问题
不过rust并不是通过运行时去自动解决的,它是通过加上强限制,让程序员写代码的时候更加谨慎和小心一点,最终还是把问题抛给了程序员的。
当程序员克服了这个问题,后期的维护成本和线上风险就降低了很多。