写点什么

Rust 从 0 到 1- 所有权 - 引用和借用

用户头像
关注
发布于: 2021 年 04 月 02 日
Rust从0到1-所有权-引用和借用

书接上回,如果要在将变量作为参数传递给调用函数后仍然能使用该变量,而不用再次返回,也就是说只是引用这个变量作为参数而不是获取值的所有权,应该怎么办呢?参考下面的例子:

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.len()}
复制代码

在这里我们使用 &s1 将变量作为参数传给 calculate_length 函数,同时在函数定义中,我们使用 &String ,而不是 String

这些 & 符号就是“引用(reference)”,它允许你使用值但不获取其所有权(相反的还有一个解引用符 * 这个在后面章节会详细介绍)。如下图所示(来自官网):


&s1 让我们创建一个 指向值 s1 的引用,但是并不拥有它。因为并不拥有这个值,当 s 离开作用域时其指向的值也不会被丢弃。同样,函数定义中也需要使用 & 来表明参数 s 的类型是一个引用。

Rust 将使用引用作为函数参数的行为称为“借用(borrowing)”。就像我们从别人那里借用某样东西一样,你可以借过来使用,但是当你使用完以后,必须归还。那么,如果我们尝试修改借用的变量会发生什么呢?参考下面的例子(官网的例子我觉得举的不太好,我稍微修改了下,请大家注意):

fn main() {  let s1 = String::from("hello");  change(&s1);}
fn change(mut s: &String) { s.push_str(", world"); println!("{}",s);}
复制代码

可变引用

那么我们如果要修改引用的值怎么办呢?参考下面的例子:

fn main() {  let mut s1 = String::from("hello");  change(&mut s1);}fn change(s: &mut String) {  s.push_str(", world");  println!("{}",s);}
复制代码

请大家注意比较两个例子的不同。

不过可变引用有一个很大的限制:在所有者的作用域中在同一时刻有且只能有一个可变引用存在,参考下面的例子:

fn main() {  let mut s = String::from("hello");
let r1 = &mut s; let r2 = &mut s; //不允许,编译错误
println!("{}, {}", r1, r2);}
复制代码

这个限制的好处是 Rust 可以在编译时就避免数据竞争(data race)。数据竞争在具备以下三个条件时就会发生:

  • 两个或更多指针同时访问同一数据。

  • 至少有一个指针被用来写入数据。

  • 没有同步数据访问的机制。

数据竞争可能会导致未定义行为,并且难以在运行时追踪,难以诊断和修复;Rust 避免了这种情况的发生,因为它不会编译存在数据竞争的代码!

我们可以使用大括号来创建一个新的作用域,这样就可以允许拥有多个可变引用,只是不能“同时”拥有:

fn main() {  let mut s = String::from("hello");  {      let r1 = &mut s;  } // r1 作用域结束,变为无效,因此后面可以创建 r2  let r2 = &mut s;}
复制代码

类似的限制也存在于同时使用可变与不可变引用中,但是我们可以同时拥有多个不可变引用:

fn main() {  let mut s = String::from("hello");
let r1 = &s; // 没问题 let r2 = &s; // 没问题 let r3 = &mut s; // 有问题,编译错误
println!("{}, {}, and {}", r1, r2, r3);}
复制代码

我们不能在拥有不可变引用的同时拥有可变引用。使用不可变引用一般是我们希望值不被改变,如果同时又使用了可变引用,那预期的结果可能并不是我们想要的。但是,多个不可变引用是可以的,因为不可变引用相当于是只读数据,不会对预期结果造成影响。

另外,需要注意的是引用的作用域是从声明的地方开始一直持续到最后一次使用为止,这个与变量不同。因此,在最后一次使用不可变引用之后再声明可变引用是可以的:

fn main() {  let mut s = String::from("hello");
let r1 = &s; let r2 = &s; println!("{} and {}", r1, r2); // r1 and r2 不再使用 let r3 = &mut s; println!("{}", r3);}
复制代码

不可变引用 r1 和 r2 的作用域在 println! 最后一次使用之后结束,因此后面可以再创建可变引用 r3 。它们的作用域没有重叠,所以代码是可以正确编译并运行的。

悬空引用

Dangling References,有些翻译为悬垂引用、摇摆引用,我觉得悬空可能更合适,说明它很危险;)。在具有指针的语言中(譬如 C),很容易通过释放内存时保留指向它的指针而错误地生成一个悬空指针(dangling pointer),所谓悬空指针是指其指向的内存可能已经被分配给其它持有者。而在 Rust 中,编译器不允许发生这种事情,它会确保数据不会在其引用结束之前离开作用域:

fn main() {  let reference_to_nothing = dangle();}
fn dangle() -> &String { //返回一个字符串的引用 let s = String::from("hello"); //声明变量 s &s //返回变量 s 的引用}
复制代码

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

引用的规则

概括之前对引用的讨论:


  • 在任意给定时间,只能有一个可变引用或者多个不可变引用。

  • 引用必须总是有效的。


下一章将介绍另一种不同类型的引用:切片(Slice)。

发布于: 2021 年 04 月 02 日阅读数: 16
用户头像

关注

公众号"山 顽石"欢迎大家关注;) 2021.03.23 加入

IT老兵,关注Rust、Java、前端、架构、大数据、AI、算法等;平时喜欢美食、旅游、宠物、健身、篮球、乒乓球等。希望能和大家交流分享。

评论

发布
暂无评论
Rust从0到1-所有权-引用和借用