Rust 基础入门 —— 2.3.所有权和借用 (二)

简介: 同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用引用必须总是有效的贴一个体验不错的学习链接恰饭:学习链接

引用和借用

我们之前说了很多,总结一句话:所有权,让代码可以免去内存回收的问题,但不方便开发。解决方式,就是引用和借用。


这里我们具体介绍这个概念。


是什么?

引用和借用是一个事情,表示的是通过语法,告诉 编译器,不要改动所有权。只是一个代称。你可以叫它引用,也可以叫他借用。是一回事情。当然官方的说法是:获取变量的引用,称之为借用(borrowing)


为什么的问题,已经在开头说明了。便于开发。

怎么做?

这里我们分四块介绍:


  • 引用和解引用
  • 不可变引用
  • 可变引用
  • 悬垂引用

引用和解引用

废话不说,上代码


let z = String::from("hello word");
let r = &z; // 这里就是引用了
println!("{},{}, {}, {}",z,r,*r,&z);
assert_eq!("hello word", z.to_string());// 这两者写法都可以
assert_eq!("hello word", z);// 这两者写法都可以
assert_eq!("hello word", *r);


上面的代码中 z 中存放了原始的数据,r是 z的一个引用。可以断言 z 等于 hello word,但是想要对 r的值进行断言,就必须使用*r来解出 引用所指向的值(也就是解引用)。一旦解引用了r就可以访问r的值,


那么既然 对 z可以使用两种方式,那么解引用的方式呢?


PS ...\ZryCode\CODE\Rust\file23_08_21> cargo run
   Compiling file23_08_21 v0.1.0 (...\ZryCode\CODE\Rust\file23_08_21)
error[E0277]: can't compare `&str` with `str`
  --> src\main.rs:69:5
   |
69 |     assert_eq!("hello word", *r.to_string());
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no implementation for `&str == str`
   |
   = help: the trait `PartialEq<str>` is not implemented for `&str`
   = help: the following other types implement trait `PartialEq<Rhs>`:
             <&'a str as PartialEq<OsString>>
             <&'a str as PartialEq<String>>
             <&'b str as PartialEq<Cow<'a, str>>>
             <str as PartialEq<Cow<'a, str>>>
             <str as PartialEq<OsStr>>
             <str as PartialEq<OsString>>
             <str as PartialEq<String>>
             <str as PartialEq>
   = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `file23_08_21` due to previous error


另外补充:

let s = String::from("hello");
let s1 = &s;
let s2 = &s1;
let s3 = &s2;
println!("{}", s3.len());
println!("{}", *s3);   // 可以正常展示 hello
println!("{}", ***s3); // 依旧可以正常展示  hello


所以,我们可以认为 解引用不关心引用了几次,会一直找到原始数据源为止。


在介绍了引用和解引用之后,我们只是了解到了简单的使用,具体一点的,引用分为三种


不可变引用

就是说我的目的只是传值,不想改变值得内容。


用途:多用于 函数传值,

核心思想:我拿来借一下就好,我不想管理数据。


具体代码:


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()
}


注意到两点:


无需像之前一样:先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁

calculate_length 的参数 s 类型从 String 变为 &String

这里,& 符号即是引用,它们允许你使用值,但是不获取所有权,如图所示:

![[Pasted image 20230821170647.png]]

通过 &s1 语法,我们创建了一个指向 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。


同理,函数 calculate_length 使用 & 来表明参数 s 的类型是一个引用:


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


可变引用

我们在c++ 中形参不单是 const 还要 可变 形参, 在rust中也有。我们称之为可变引用。


在rust中依然使用得是mut 关键字。


具体代码如下:

fn main(){
    let  mut zry = String::from("hello");
    change(&mut zry);
    println!("{}",zry);
}
fn change(some_string: &mut String)
{
    some_string.push_str(" word");
}


首先,声明 zry 是可变类型,其次创建一个可变的引用 &mut zry 和接受可变引用参数 some_string: &mut String 的函数。


重点一


可变引用同时只能存在一个:你可以认为——当我想要改变一个对象时,我不希望别人也可以改变它。 rust 从编译器层面上保证了这个事情。

具体来讲:

fn main(){
    let  mut zry = String::from("hello");
    change(&mut zry);
    println!("{}",zry);
    let r1 = &mut zry;
    let r2 = &mut zry;
    println("{} {}",r1, r2);
}
fn change(some_string: &mut String)
{
    some_string.push_str(" word");
}


这样的代码会出现问题:因为它 违反了上述的:可变引用同时只能存在一个的原则。

报错如下:


error[E0499]: cannot borrow `zry` as mutable more than once at a time
   --> src\main.rs:107:14
    |
106 |     let r1 = &mut zry;
    |              -------- first mutable borrow occurs here
107 |     let r2 = &mut zry;
    |              ^^^^^^^^ second mutable borrow occurs here
108 |     println!("{} {}",r1, r2);
    |                      -- first borrow later used here


这段代码出错的原因在于,第一个可变借用 r1 必须要持续到最后一次使用的位置 println!,在 r1 创建和最后一次使用之间,我们又尝试创建第二个可变借用 r2。


对于新手来说,这个特性绝对是一大拦路虎,也是新人们谈之色变的编译器 borrow checker 特性之一,不过各行各业都一样,限制往往是出于安全的考虑,Rust 也一样。


这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:


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

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


很多时候,大括号可以帮我们解决一些编译不通过的问题,通过手动限制变量的作用域:


fn main(){
    let  mut zry = String::from("hello");
    change(&mut zry);
    println!("{}",zry);
    {
        let r1 = &mut zry;
    }
    let r2 = &mut zry;
    println("{}", r2);
}
fn change(some_string: &mut String)
{
    some_string.push_str(" word");
}


这样就好了


重点二


可变引用与不可变引用不能同时存在 —— 其本质是指:你又想一个东西不变,又想一个东西能让你变,这件事本身就是矛盾的。

error[E0502]: cannot borrow `zry` as immutable because it is also borrowed as mutable
   --> src\main.rs:110:14
    |
109 |     let r2 = &mut zry;
    |              -------- mutable borrow occurs here
110 |     let r3 = &zry;
    |              ^^^^ immutable borrow occurs here
111 |     println!("{} ",  r2);
    |                      -- mutable borrow later used here


多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染


补充说明:


注意,引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }

悬垂引用

悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。


让我们尝试创建一个悬垂引用,Rust 会抛出一个编译时错误:

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}


这里是错误:


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 {
  |                ~~~~~~~~


错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。不过,即使你不理解生命周期,也可以通过错误信息知道这段代码错误的关键信息:


this function's return type contains a borrowed value, but there is no value for it to be borrowed from. 该函数返回了一个借用的值,但是已经找不到它所借用值的来源


仔细看看 dangle 代码的每一步到底发生了什么:


fn dangle() -> &String { // dangle 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串
    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!


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


其中一个很好的解决方法是直接返回 String:


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


这样就没有任何错误了,最终 String 的 所有权被转移给外面的调用者。


总结:


引用和借用

我们之前说了很多,总结一句话:所有权,让代码可以免去内存回收的问题,但不方便开发。解决方式,就是引用和借用。


这里我们具体介绍这个概念。


是什么?

引用和借用是一个事情,表示的是通过语法,告诉 编译器,不要改动所有权。只是一个代称。你可以叫它引用,也可以叫他借用。是一回事情。当然官方的说法是:获取变量的引用,称之为借用(borrowing)


为什么的问题,已经在开头说明了。便于开发。


怎么做?

这里我们分四块介绍


目录
相关文章
|
1月前
|
Rust 安全 Java
编程语言新宠:Rust语言的特性、优势与实战入门
【10月更文挑战第27天】Rust语言以其独特的特性和优势在编程领域迅速崛起。本文介绍Rust的核心特性,如所有权系统和强大的并发处理能力,以及其性能和安全性优势。通过实战示例,如“Hello, World!”和线程编程,帮助读者快速入门Rust。
72 1
|
1月前
|
Rust 安全 编译器
编程语言新宠:Rust语言的特性、优势与实战入门
【10月更文挑战第26天】Rust语言诞生于2006年,由Mozilla公司的Graydon Hoare发起。作为一门系统编程语言,Rust专注于安全和高性能。通过所有权系统和生命周期管理,Rust在编译期就能消除内存泄漏等问题,适用于操作系统、嵌入式系统等高可靠性场景。
83 2
|
1月前
|
Rust 安全
深入理解Rust语言的所有权系统
深入理解Rust语言的所有权系统
36 0
|
1月前
|
Rust 安全 云计算
Rust语言入门:安全性与并发性的完美结合
【10月更文挑战第25天】Rust 是一种系统级编程语言,以其独特的安全性和并发性保障而著称。它提供了与 C 和 C++ 相当的性能,同时确保内存安全,避免了常见的安全问题。Rust 的所有权系统通过编译时检查保证内存安全,其零成本抽象设计使得抽象不会带来额外的性能开销。Rust 还提供了强大的并发编程工具,如线程、消息传递和原子操作,确保了数据竞争的编译时检测。这些特性使 Rust 成为编写高效、安全并发代码的理想选择。
29 0
|
4月前
|
存储 Rust 安全
30天拿下Rust之所有权
在编程语言的世界中,Rust凭借其独特的所有权机制脱颖而出,为开发者提供了一种新颖而强大的工具来防止内存错误。这一特性不仅确保了代码的安全性,还极大地提升了程序的性能。在Rust中,所有权是一种编译时检查机制,用于追踪哪些内存或资源何时可以被释放。每当一个变量被赋予一个值(比如:字符串、数组或文件句柄)时,Rust会确定这个变量是否“拥有”这个值,拥有资源的变量负责在适当的时候释放这些资源。
43 5
|
5月前
|
存储 Rust 安全
【Rust学习】04_所有权
所有权是 Rust 最独特的特性,对语言的其余部分有着深远的影响。它使 Rust 能够在不需要垃圾收集器的情况下保证内存安全,因此了解所有权的运作方式非常重要。在本章中,我们将讨论所有权以及几个相关功能:借用、切片以及 Rust 如何在内存中布局数据。
33 1
|
6月前
|
Rust 安全 开发者
Rust引用、借用和所有权详解
Rust引用、借用和所有权详解
|
1月前
|
Rust 安全 Java
探索Rust语言的并发编程模型
探索Rust语言的并发编程模型
|
1月前
|
Rust 安全 区块链
探索Rust语言:系统编程的新选择
【10月更文挑战第27天】Rust语言以其安全性、性能和并发性在系统编程领域受到广泛关注。本文介绍了Rust的核心特性,如内存安全、高性能和强大的并发模型,以及开发技巧和实用工具,展示了Rust如何改变系统编程的面貌,并展望了其在WebAssembly、区块链和嵌入式系统等领域的未来应用。
|
1月前
|
Rust 安全 前端开发
探索Rust语言的异步编程模型
探索Rust语言的异步编程模型