Rust学习之旅——所有权和数据借用

前言

相较于别的编程语言,Rust具有一套单独的内存管理规范。

所有权

实例化一个类型并且将其绑定到变量名上将会创建一些内存资源,而这些资源将会在其整个生命周期中被Rust编译器检验。被绑定的变量即为该资源的所有者。

struct Foo {
	x: i32,
}

fn main() {
	// 我们实例化这个结构体并将其绑定到具体的变量上
	// 创建内存资源
	let foo = Foo { x: 42 };
	// foo即为该资源的所有者
}

基于域的资源管理

Rust将使用资源最后被使用的位置或者一个函数域的结束来作为资源被析构和释放的地方。此处析构和释放的概念被称之为drop(丢弃)。
内存细节:

  • Rust没有垃圾回收机制
  • 在C++中,这也被称为“资源获取即初始化”。
struct Foo {
	x: i32,
}

fn main() {
	let foo_a = Foo { x: 42 };
	let foo_b = Foo { x: 13 };

	println!("{}", foo_a.x);
	// foo_a 将在这里被 dropped 因为其在这之后再也没有被使用过

	println!("{}", foo_b.x);
	// foo_b 将在这里被 dropped 因为这是函数域的结尾
}

释放是分级进行的

删除一个结构体时,结构体本身会先被释放掉,紧接着才分别释放相应的子结构体并以此类推。
内存细节:

  • Rust通过自动释放内存来帮助减少内存泄漏
  • 每个内存资源仅会被释放一次
struct Bar {
	x: i32,
}

struct Foo {
	bar: Bar,
}

fn main() {
	let foo = Foo { bar: Bar { x: 42 } };
	println!("{}", foo.bar.x);
	// foo 首先被 dropped 释放掉
	// 紧接着是 foo.bar
}

移交所有权

将所有者作为参数传递给函数时,其所有权将移至该函数的参数。再一次移动后,原函数中的变量将无法在被使用。

内存细节:

  • 在移动期间,所有者的堆栈值将会被复制到函数调用的参数堆栈中。
struct Foo {
	x: i32,
}

fn do_something(f: Foo) {
	println!("{}", f.x);
	// f 在这里被 dropped 释放
}

fn main() {
	let foo = Foo { x: 42 };
	// foo 被移交至 do_something
	do_something(foo);
	// 此后 foo 便无法在被使用
}

归还所有权

所有权也可以从一个函数中被归还

struct Foo {
	x: i32,
}

fn do_something() -> Foo {
	Foo { x: 42 }
	// 所有权被移出
}

fn main() {
	let foo = do_something();
	// foo这里成为了所有者
	// foo在函数域结尾被 dropped 释放
}

使用引用借用所有权

引用允许我们通过 & 操作符来借用对一个资源的访问权限。引用也会如同其他资源一样被释放。

struct Foo {
	x: i32,
}

fn main() {
	let foo = Foo { x: 42 };
	let f = &foo;
	println!("{}", f.x);
	// f 在这里被dropped释放
	// foo 在这里被dropped释放
}

通过引用借用可变所有权

我们通过 &mut 操作符来借用一个资源的可变访问权限。在发生了可变借用之后,一个资源的所有者便不可以再次被借用或者修改。

内存细节:

  • Rust之所以要避免同时存在两种可以改变所有者变量值的方式,是因为此举可能会导致潜在的数据争用(data race)
struct Foo {
	x: i32,
}
fn do_something(f: Foo) {
	println!("{}", f.x);
	// f 在这里被 dropped 释放
}
fn main() {
	let mut foo = Foo { x: 42 };
	let f = &mut foo;
	// 直接do_something(foo)会报错
	// 因为 foo 已经被可变借用而无法取得其所有权

	//直接 foo.x = 13也会报错
	// 因为foo已经被可变借用而无法被修改
	
	f.x = 13;
	// f因为不会在被使用而被dropped释放
	println!("{}", foo.x);
	
	//现在可以被修改因为其可变引用已经被释放
	foo.x = 7;
	//移动 foo 的所有权到一个函数中
	do_something(foo);
}

解引用

使用 &mut 引用时,你可以通过 * 操作符来修改其指向的值。你也可以使用 * 操作符来对所拥有的值进行拷贝(前提是该值可以被拷贝)

fn main() {
	let mut foo = 42;
	let f = &mut foo;
	let bar = *f;		// 取得所有者值的拷贝
	*f = 13;			// 设置引用所有者的值
	println!("{}", bar);
	pritln!("{}", foo);
}

传递借用的数据

Rust 对于引用的规则也许最好用以下的方式总结:

  • Rust 只允许同时存在一个可变引用或者多个不可变引用,不允许可变引用和不可变引用同时存在。
  • 一个引用永远不会比它的所有者存活时间更久。
    而在函数间进行引用的传递时,以上这些通常都不会成为问题。

内存细节:

  • 上面第一条规则避免了数据争用。数据争用就是可能会因为同时存在对数据的写入而产生不同步。这一点往往出现在多线程中。
  • 而第二条引用规则则避免了通过引用而错误的访问到不存在的数据(在C语言中被称为悬垂指针)。
struct Foo {
	x: i32,
}

fn do_something(f: &mut Foo) {
	f.x += 1;
	// 可变引用 f 在这里被dropped释放
}

fn main() {
	let mut foo = Foo { x: 42 };
	do_something(&mut foo);
	// 因为所有的可变引用都在 do_something 函数内部被释放了
	// 此时我们便可以再创建一个
	do_something(&mut foo);
	// foo 在这里被释放
}

引用的引用

引用甚至也可以作用在其他引用上

struct Foo {
	x: i32,
}

fn do_something(a: &Foo) -> &i32 {
	return &a.x;
}

fn main() {
	let mut foo = Foo { x: 42 };
	let x = &mut foo.x;
	*x = 13;
	// x在这里被释放,从而允许我们再新建一个不可变引用
	let y = do_something(&foo);
	println!("{}", y);
	// y在这里被释放
	// foo在这里被释放
}

显式生命周期

尽管Rust不总是在代码中将它展现出来,但是编译器会理解每一个变量的生命周期并进行验证以确保一个引用不会有长于其所有的存在时间。同时,函数可以通过使用一些符号来参数化函数签名,以帮助界定哪些参数和返回值共享同一生命周期。生命周期注解总是以 ’ 开头,例如 'a, 'b 以及 'c。

struct Foo {
	x: i32,
}

fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
	return &foo.x;
}

fn main() {
	let mut foo = Foo { x: 42 };
	let x = &mut foo.x;
	*x = 13;
	// x在这里被释放,从而允许我们再新建一个不可变引用
	let y = do_something(&foo);
	println!("{}", y);
	// y在这里被释放
	// foo在这里被释放
}

多个生命周期

生命周期注解可以通过区分函数签名周不同部分的生命周期,来允许我们显式地明确某些编译器自己无法解决的场景。

struct Foo {
	x: i32,
}

// foo_b 和返回值共享同一生命周期
// foo_a 则拥有另外一个不相关的生命周期
fn do_something<'a, 'b>(foo_a: &'a Foo, foo_b: &'b foo) -> &'b i32 {
	println!("{}", foo_a.x);
	println!("{}", foo_b.x);
	return &foo_b.x;
}

fn main() {
	let foo_a = Foo { x: 42 };
	let foo_b = Foo { x: 12 };
	let x = do_something(&foo_a, &foo_b);
	// foo_a这里被释放,只有foo_b在此之后还在延续
	println!("{}", x);
	// x在这里先被释放
	// foo_b在这里被释放
}

静态生命周期

一个静态生命周期是一个在编译期间即被创建并存在于整个程序始末的内存资源。他们必须被明确指明类型。一个静态生命周期是指一段内存资源无限期地延续到程序结束。需要注意的一点是,在此定义下,一些静态生命周期地资源也可以在运行时被创建。拥有静态生命周期地资源会拥有一个特殊地生命周期注解 'static。'static 资源永远也不会被drop释放。如果静态生命周期包含了引用,那么这些引用的生命周期也一定是 'static 的。(任何缺少了此注解的引用都不会达到同样长的存活时间)

内存细节:

  • 引用静态变量可以全局性地被任何人访问读取而潜在地引入数据争用,所以修改它具有内在的危险性。
  • Rust允许使用 unsafe {…} 代码块来进行一些无法被编译器担保的内存操作。
static PI: f64 = 3.1415;

fn main() {
	// 静态变量的范围也可以被限制在一个函数里
	static mut SECRET: &'static str = "swordfish";
	// 字符串字面值拥有 'static 生命周期
	let msg: &'static str = "Hello World!";
	let p: &'static f64 = &PI;
	println!("{} {}", msg, p);
	// 可以打破一些规则,但是必须是显式地
	unsafe {
		// 我们可以修改 SECRET 到一个字符字面值
		SECRET = "abracadabra";
		println!("{}", SECRET);
	}
}

数据类型中的生命周期

和函数相比,数据类型也可以用生命周期注解来参数化其成员。Rust会验证引用所包含的数据结构永远不会比引用指向的所有者存活时间更长。我们不能在运行中拥有一个包括指向虚无的引用结构存在!

struct Foo<'a> {
	i:&'a i32
}

fn main(){
	let x = 42;
	let foo = Foo {
		i: &x
	};
	println!("{}", foo.i);
}

总结

可以看到Rust解决了系统编程中的诸多挑战:

  • 无意间对资源的修改
  • 忘记及时地释放资源
  • 资源意外地被释放了两次
  • 在资源被释放后使用它
  • 由于读取数据的同时有其他人正在向资源中写入数据而引起的数据争用
  • 在编译器无法做担保时,清晰地看到代码的作用域
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>