测试代码位于gitee仓库:https://gitee.com/linysuccess/rust0_100
通过前面几个小节的介绍,我们对Rust语言代码的样貌有了大致的印象。但其实,至今为止我们仍然停留在Rust写写简单代码的表象,如果不搞清楚Rust核心的所有权机制和变量生命周期等问题,后续将很难深入。
这一节,我们就先来谈谈Rust中的“所有权”。
Rust所有权机制的设计是这门语言的最大特点,也是其内存安全和多线程并发安全的基石。
struct Point {
x: i32,
y: i32,
}
这就是Rust所有权机制的核心概念。
在核心概念之上,rust针对不同的使用场景衍生出了一些子规则:
fn test_ownership() {
// 只读的p1指向一个在栈上分配的Point结构
let p1 = Point { x: 25, y: 25 };
// 将p1指向的point的所有权转移给p2
let p2: Point = p1;
// p1已无效,只能通过p2进行访问
// println!("{} {}", p2.x, p2.y);
}
也有不发生所有权转移(move)的使用场景,比如基本的值类型i32、u8
等。事实上是,如果一个类型实现了Copy trait,那么赋值、传参等操作的默认行为不是move所有权,而是复制一份新内容出来。
let mut x = 1;
let y = 10;
// i32类型实现了Copy trait
// 这里的赋值,是复制行为
x = y;
// 打印y和x的地址,发现y和x地址不同
println!("{:p}", &y);//0xc4236fee5c
println!("{:p}", &x);//0xc4236fee58
// 通过复制,但两个地址上存放的值都是10
// 并且,y依然有自己的所有权
println!("y={}", y);//10
println!("x={}", x);//10
一个值除了有唯一的所有者之外,还可以有多个“借用(borrow)者”。借用者无须关心值内存的释放问题,也不必担心值内存已被释放(C++中悬垂指针)的问题。因为,编译器会确保对值的借用不会超出值本身的生命周期。
Rust中的借用类比C里面的引用,也用&符标注。只是为了突出rust中的这个“引用”不具有所有权,用“借用”表述更明确。
let p: Point = Point {x:1, y:1, };
// q是对p所指内容的借用
let mut q: &Point = &p;
// 所有权仍然归p
println!("p.x={}", p.x);
println!("q.x={}", q.x);
{
// r指向栈上分配的Point数据
let r = Point{x:10, y:10, };
// 下面这句编译报错:borrowed value does not live long enough
// r作为所有者,在花括号结束之后,生命周期就结束了
// q的生命周期,要到main函数结束
// q的生命周期更长,不能借用生命周期更短的r
q = &r;
}
println!("q.x={}", q.x);
当所有者离开作用域,其拥有的值被丢弃,内存得以释放。
因为单一所有权的约束,编译器能准确跟踪所有者的生命周期。从而,在所有者生命周期结束时,编译器自动插入释放代码完成堆内存的释放(实际是调用所有者实现的Drop trait
中的方法)。
在一个作用域中,对同一个值的借用:
//代码片段1 多个只读借用,可以共存
let mut x = "Hello".to_string();
let x_b0 = &x;
let x_b1 = &x;
println!("{:?}", x_b0);
println!("{:?}", x_b1);
//代码片段2 有多个引用,但都不“活跃”,编译通过
let mut x = "Hello".to_string();
let x_b0 = &x;
let x_b1 = &x;
let mut x_b2 = &mut x;
let mut x_b3 = &mut x;
// 代码片段3 x_b0已经是“活跃”的只读借用,不允许再出现可写借用
let mut x = "Hello".to_string();
let x_b0 = &x;
let x_b1 = &x;
// 下面一行编译报错:
// cannot borrow `x` as mutable because it is also borrowed as immutable
let mut x_b2 = &mut x;
println!("{:?}", x_b0);
// 代码片段4
// 只有1个活跃的可写借用,2个只读借用不活跃,编译通过
let mut x = "Hello".to_string();
let x_b0 = &x;
let x_b1 = &x;
let mut x_b2 = &mut x;
x_b2.push_str("World");
// 代码片段5 可写借用不应该有多个
let mut x = "Hello".to_string();
let mut x_b2 = &mut x;
// 下面一行编译报错
// cannot borrow `x` as mutable more than once at a time
let mut x_b3 = &mut x;
x_b2.push_str("World");
let
声明的变量默认是read-only,类比java中的final。如果要能修改内存中的值,需要借助mut
关键字进行声明。
fn test_mut() {
// 想要通过变量修改指向的内存值,需要mut关键字配合
let mut p1 = Point { x: 25, y: 25 };
p1.x = 30;
println!("{} {}", p1.x, p1.y);
}
上面介绍了赋值场景下,所有权的转移和借用问题。当然,函数调用传参,所有权的转移和借用也是一样的。
fn test_borrow() {
let p1 = Point { x: 25, y: 25 };
// 通过&符将原数据的使用权,借用到borrowReadOnly()函数里面
borrowReadOnly(&p1);
// p1仍然具有所有权
println!("{}", p1.x);
}
// p是只读借用,f里面不能修改p.x/p.y
// p只是一个借用,不会延长原数据的生命周期
fn borrowReadOnly(p: &Point) {
println!("{}", p.x);
}
如果想要在函数里面,修改传参变量所引用的内存,需要借助mut
。
// 可写借用,借用者可以对内存进行修改
// 借用只是一个使用权,并不具备所有权
fn test_mut_borrow() {
let mut p = Point { x: 25, y: 25 };
borrowWritable(&mut p);
println!("{}", p.x);// 100
}
fn borrowWritable(p: &mut Point) {
p.x = 100;
}
可写的借用必须3个地方都同时加mut
才行:p变量声明、函数参数签名、函数调用传参。
测试代码位于gitee仓库:https://gitee.com/linysuccess/rust0_100