引用与借用

引用与借用

Owership 让我们改变一个变量的值变得“复杂”,那能否像其他编程语言那样随意改变变量的值呢?答案是有的。所有权系统允许我们通过“Borrowing”的方式达到这个目的。这个机制非常像其他编程语言中的“读写锁”,即同一时刻,只能拥有一个“写锁”,或只能拥有多个“读锁”,不允许“写锁”和“读锁”在同一时刻同时出现。当然这也是数据读写过程中保障一致性的典型做法。只不过 Rust 是在编译中完成这个(Borrowing)检查的,而不是在运行时,这也就是为什么其他语言程序在运行过程中,容易出现死锁或者野指针的问题。

引用

通过 & 符号完成 Borrowing:

fn main() {
	let x: Vec<i32> = vec![1i32, 2, 3];
	let y = &x;
	println!("x={:?}, y={:?}", x, y);
}

Borrowing(&x)并不会发生所有权 moved,所以 println 可以同时访问 x 和 y。通过引用,就可以对普通类型完成修改。

fn main() {
	let mut x: i32 = 100;
	{
		let y: &mut i32 = &mut x;
		*y += 2;
	}
	println!("{}", x);
}

我们再来看下函数调用中借用的用法,定义并使用一个(新的)calculate_length 函数,它以一个对象的引用作为参数而不是获取值的所有权:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
  // 所以什么也不会发生

首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 &s1 给 calculate_length,同时在函数定义中,我们获取 &String 而不是 String。

&amp;String s 指向 String s1 示意图

&s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。变量 s 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据,因为我们没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。我们将获取引用作为函数参数称为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。

借用与引用是一种相辅相成的关系,若 B 是对 A 的引用,也可称之为 B 借用了 A。

可变借用

Borrowing 也分“不可变借用”(默认,&T)和“可变借用”(&mut T)。顾名思义,“不可变借用”是只读的,不可更新被引用的内容。

fn main() {
	let x: Vec<i32> = vec!(1i32, 2, 3);

	//可同时有多个不可变借用
	let y = &x;
	let z = &x;
	let m = &x;

	//ok
	println!("{:?}, {:?}, {:?}, {:?}", x, y, z, m);
}

再次强调下,同一作用域下只能有一个可变借用(&mut T),且被借用的变量本身必须有可变性:

  • 同一作用域,特定数据最多只有一个可变借用(&mut T),或者 2。
  • 同一作用域,特定数据可有 0 个或多个不可变借用(&T),但不能有任何可变借用。
  • 借用在离开作用域后释放。
  • 在可变借用释放前不可访问源变量。
fn main() {
	//源变量x可变性
	let mut x: Vec<i32> = vec!(1i32, 2, 3);

	//只能有一个可变借用
	let y = &mut x;
	// let z = &mut x; //错误
    y.push(100);

	//ok
	println!("{:?}", y);

	//错误,可变借用未释放,源变量不可访问
	// println!("{:?}", x);
}  //y在此处销毁

借用不改变内存的所有者(Owner),借用只是对源内存的临时引用。在借用周期内,借用方可以读写这块内存,所有者被禁止读写内存;且所有者保证在有“借用”存在的情况下,不会释放或转移内存。

失去所有权的变量不可以被借用(访问)。在租借期内,内存所有者保证不会释放/转移/可变租借这块内存,但如果是在非可变租借的情况下,所有者是允许继续非可变租借出去的。借用周期满后,所有者收回读写权限,并且借用周期小于被借用者(所有者)的生命周期。

fn main() {
	let mut x: Vec<i32> = vec!(1i32, 2, 3);

	//更新数组
	// push 中对数组进行了可变借用,并在 push 函数退出时销毁这个借用
    x.push(10);

    {
	    // 可变借用 1
	    let mut y = &mut x;
        y.push(100);

        // 可变借用 2,注意:此处是对 y 的借用,不可再对 x 进行借用,
        // 因为 y 在此时依然存活。
        let z = &mut y;
        z.push(1000);

	    println!("{:?}", z); //打印: [1, 2, 3, 10, 100, 1000]
    } // y 和 z 在此处被销毁,并释放借用。


	//访问x正常
	println!("{:?}", x); //打印: [1, 2, 3, 10, 100, 1000]
}

注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用在声明可变引用之前,所以如下代码是可以编译的:

let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用

let r3 = &mut s; // 没问题
println!("{}", r3);

不可变引用 r1 和 r2 的作用域在 println! 最后一次使用之后结束,这也是创建可变引用 r3 的地方。它们的作用域没有重叠,所以代码是可以编译的。

悬垂引用(Dangling References)

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

fn dangle() -> &String { // dangle 返回一个字符串的引用

    let s = String::from("hello"); // s 是一个新字符串

    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!

error[E0106]: missing lifetime specifier
 --> main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is
  no value for it to be borrowed from
  = help: consider giving it a 'static lifetime

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!Rust 不会允许我们这么做。

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}
上一页