Rust - 可变引用和悬垂引用

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

可变引用

在上一篇文章中,我们提到了借用的概念,将获取引用作为函数参数称为 借用borrowing),通常情况下,我们无法修改借来的变量,但是可以通过可变引用实现修改借来的变量。代码示例如下:

fn main() {
    let mut s = String::from("hello");  // s是可变的变量
    
    change(&mut s);  // &mut 表示可变引用
}

fn change(some_string: &mut String) {  // &mut 表示可变引用
    some_string.push_str(", world");
}
复制代码

要想实现修改借来的变量就必须将 s 改为 mut。然后必须创建一个可变引用 &mut s 和接受一个可变引用 some_string: &mut String

但是可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用。比如下述代码就不会被成功编译。

fn main() {
    let mut s = String::from("hello"); 
    
    let r1 = &mut s;
    let r2 = &mut s;
}
复制代码

编译运行就会抛出如下异常:

error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here
复制代码

所以这种修改借来的变量的可变引用是以一种受限制的方式允许修改,这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争data race)类似于竞态条件,它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生。我们可以使用{}创建一个新的作用域,这样就能够允许多个可变引用了,只是不能在同一个作用域中同时拥有:

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

	{
    	let r1 = &mut s;

	} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

	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);

} 
复制代码

上面代码示例编译时会抛出如下异常:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here
复制代码

但是如果可变引用和不可变引用他们的作用域不重叠代码就是可以编译的,我们可以将上面的代码示例进行修改就可以正常运行了。

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

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

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

}
复制代码

悬垂引用(Dangling References

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

当我们不小心创建了悬垂引用,Rust在编译的时候就会抛出异常:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {  // dangle 返回一个字符串的引用
    let s = String::from("hello");  // s 是一个新字符串

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

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String,所以在编译时Rust就会抛出异常,解决方式就是直接返回String

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

    s
}  // 所有权被移动出去,内存没有被释放
复制代码

结语

文章首发于微信公众号程序媛小庄,同步于掘金

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

猜你喜欢

转载自juejin.im/post/7034369064764080142