0

所有权这块知识量太大了,慢慢嚼(头秃

引用和借用

知识回顾:

...以上省略
Rust 允许我们使用元组返回多个值:

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

    let (s2, len) = calculate_length(s1);

    // s1 已经被移动,无法使用

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

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字符串的长度

    (s, length)
}

Rust 有一个使用值而不转移所有权的功能,称为引用


上述代码中,存在一个问题:我们必须将String值返回给调用函数,才能在调用calculate_length函数之后继续使用该 String。这是因为String在传递给calculate_length函数时被「移动」了所有权(我说呢,上一篇内容我觉得所有权还是移动了,可是文档给我说有个引用,放到这一节来讲了。gkd,继续学习),这样转来转去脑袋都糊了。

为了避免这种所有权的转移,我们可以提供一个指向String值的「引用」(reference)。引用类似于指针,它是一种地址,可以用来访问存储在该地址的数据。但与指针不同的是,引用在其生命周期内始终保证指向一个有效的、特定类型的值。

借用

以下是如何定义和使用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.len()
}

之前的代码中使用了元组来返回String和它的长度,但现在元组代码被去掉了。这意味着函数不再需要返回所有权,而是可以通过引用的方式访问数据。现在代码中传递给calculate_length函数的是&s1,这是一个指向s1的引用,而不是s1本身。在函数定义中,参数类型也从String变成了&String,表明函数接收的是一个引用:

图4-5:&String s指向String s1的示意图

* 运算符表示 解引用(Dereferencing) 操作,它是引用机制的反面。(后续知识点

想象s是一个保存值的变量(比如一本书)。它的作用域定义了它在代码中可访问的位置,类似于函数参数。

当你向函数传递&ss的引用)时,你是在 借用 这本书,而不是把它送出去。原始变量s仍然在作用域内,但函数通过引用获得了对这本书的临时访问权限。在 Rust 中,变量 拥有 其数据的所有权。当你将整个值传递给函数(例如s),所有权就转移到函数。为了 "归还" 所有权,函数通常需要返回值。

使用引用时,没有所有权转移。函数只是借用了数据,因此不需要返回任何东西来归还所有权。就好像借了一本真书一样,你通常可以在 Rust 函数中读取借用的数据。但是,你可能无法修改它(比如在书上写字)。尝试以与借用者访问冲突的方式修改借用数据(例如同时修改),通常会导致错误:

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

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

错误内容:

$ cargo run
...
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference                                                                                                    
 --> src\main.rs:8:5                                                                                                                                                                      
  |                                                                                                                                                                                       
8 |     some_string.push_str(", world");                                                                                                                                                  
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable                                                                              
  |                                                                                                                                                                                       
help: consider changing this to be a mutable reference                                                                                                                                    
  |                                                                                                                                                                                       
7 | fn change(some_string: &mut String) {                                                                                                                                                 
  |                         +++                                                                                                                                                           

For more information about this error, try `rustc --explain E0596`. 
...

以上代码,编译器不允许我们修改引用的内容。

可变引用

就像变量一样,除非使用mut关键字显式声明为可变,否则引用默认为不可变。如果需要通过引用修改数据,必须使用&mut关键字将其明确声明为可变。

上述代码,我们稍微修改下,就能运行啦:

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

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

我们将s修改为mut s,然后,我们使用&mut创建一个可变引用,在其中调用change函数,并更新函数参数以接受使用some_string: &mut String的可变引用。

可变引用 在 Rust 中具有一个重要的限制:如果您拥有一个值的可变引用,则不能再拥有该值的其他任何引用。 这意味着您不能同时拥有多个指向同一个值的可变引用:

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

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);
}

试图同时创建两个指向 s 的可变引用 (r1 和 r2)。
错误内容:

$ cargo run
...
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                                                                                                                                

For more information about this error, try `rustc --explain E0499`. 
...

此错误表明此代码无效,因为我们一次不能多次借用s作为可变对象。 第一个可变借用位于r1中,并且必须持续到在println! 中使用为止,但在该可变引用的创建和使用之间,我们尝试在r2中创建另一个可变引用,借用与r1相同的数据。

为了保证数据安全和一致性,Rust 限制了可变引用的数量。只能在同一个时间点存在一个指向同一数据的可变引用。这对于初学者来说可能比较难以理解,因为大多数语言允许随时修改数据。

不过好处就是这个限制可以帮助 Rust 在编译时就防止数据竞争。

当然,如果需要在不同时间点进行多个修改,可以使用花括号 ({}) 创建新的作用域,将修改操作封装在不同的作用域内,这使得每个作用域内允许有一个可变引用,但仍然遵守同一时间只能有一个可变引用的限制:

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

    {
        let r1 = &mut s;
        println!(" {}", r1);
    }
    let r2 = &mut s;

    println!(" {}", r2);
}

不可同时拥有可变引用和不可变引用,当你拥有一个值的可变引用时,不能再拥有该值的任何其他引用,包括不可变引用。这是为了防止数据被意外修改,并确保所有引用都指向一致的值。

如果强制这么使用:

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

错误信息:

$ cargo run
...
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                                                                                                                    

For more information about this error, try `rustc --explain E0502`. 
...

So,当我们拥有相同值的不可变引用时,我们也不能拥有可变引用

不可变引用和可变引用遵循不同规则:

  • 不可变引用 只能用于读取数据,保证数据不会在引用期间发生改变。
  • 可变引用 可以用于读取和修改数据,但会限制同时存在的引用数量。

为什么不能同时拥有可变引用和不可变引用?

  • 用户对不可变引用做出的假设是数据保持不变,如果数据突然改变,可能会导致程序行为出乎意料,引起错误。
  • 同时拥有可变引用和不可变引用无法保证数据的一致性。可变引用可以随时修改数据,使得不可变引用看到不一致的值。
  • 编译器优化也受到影响。Rust 编译器可以对不可变引用进行优化,假设数据不会改变。如果允许同时拥有可变引用,这些优化就变得不可靠。

为什么允许多个不可变引用?

  • 多个不可变引用只会读取数据,不会修改数据。
  • 它们之间不会相互影响,因为它们都只是在读取同一个不变的值。
  • 这符合数据安全和一致性的原则,多个读取不影响数据本身。

请注意,引用的范围从引入它的位置开始,一直持续到上次使用该引用时。

例如,下列代码则能进行编译,因为最后一次使用不可变引用println!发生在引入可变引用之前:

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);
}

不可变引用r1r2的范围在println!之后结束。 它们最后一次使用的位置是在创建可变引用r3之前。 这些作用域不重叠,因此允许使用此代码:编译器可以判断在作用域结束之前的某个点不再使用该引用。

虽然借用错误有时可能会令人头疼,但 Rust 编译器会尽早(在编译时而不是运行时)指出潜在的错误,并准确显示问题所在。这样就不必在运行的时候一点点排查为什么数据不是想象那样(还是很省事的。

悬空引用

在使用指针的语言中,很容易意外创建悬空引用。悬空引用是指向一块已经释放内存的内存位置的指针,访问这样的指针是无效的,并且可能导致危险的后果。在 Rust 中,编译器保证引用永远不会是悬空引用:如果您引用了某些数据,编译器将确保该数据不会在对该数据的引用之前超出范围。

我们看以下示例程序:

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

fn dangle() -> &String { // dangle 返回对 String 的引用

    let s = String::from("hello"); // s 是一个new String

    &s // 我们返回对String s 的引用
} // 此处,s 超出范围并被删除。 它的内存消失了。 危险!

错误内容:

$ cargo run
...
error[E0106]: missing lifetime specifier                                                                                                                                                  
 --> src\main.rs:5:16                                                                                                                                                                     
  |                                                                                                                                                                                       
5 | fn dangle() -> &String {                                                                                                                                                              
  |                ^ expected named 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 using the `'static` lifetime                                                                                                                                               
  |                                                                                                                                                                                       
5 | fn dangle() -> &'static String {                                                                                                                                                      
  |                 +++++++                                                                                                                                                               

For more information about this error, try `rustc --explain E0106`.
...

因为s是在dangle内部创建的,所以当dangle的代码运行结束时,s将被释放。 但我们试图返回对它的引用。 这意味着这个引用将指向一个无效的String。 这里 Rust 不允许我们这么做。

因此我们只能直接返回String

fn main() {
    let _reference_to_nothing = no_dangle();
}

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

    s
}

这样就没有任何问题了,所有权被移出,并且没有任何内容被释放。

引用规则

  • 在任何给定时间,您可以拥有一个可变引用或任意数量的不可变引用。
  • 引用必须始终有效。

接下来,我们将看看另一种不同类型的引用:切片


引用和借用暂时告一段落,下一节见

期待你一针见血的评论,Come on!

发表评论: