Fork me on GitHub

rust 程序设计笔记(2)所有权 & 引用

  • 所有权
    • 数据存储在栈和堆上,存放在栈上的数据都是已知所占据空间的
    • 突然的问题
// 内存中的栈是怎么存储数据的?
好的,想象一下你有一摞盘子。你只能从上面放盘子,也只能从上面拿盘子,这就是栈的工作方式。

在内存中,栈是用来存储数据的地方,它工作得就像这摞盘子。当你的程序运行一个函数时,这个函数的局部变量就会像放盘子一样,一个接一个地放到栈上。这些局部变量包括基本数据类型,比如整数或者布尔值,它们的大小是已知的,而且很容易快速地被放进去和拿出来。

当这个函数结束的时候,它的局部变量就会像拿盘子一样,从栈上一个接一个地被拿掉。这个过程非常快,因为它们是按顺序来的,就像你放盘子时从上面放,拿盘子时也是从上面拿。

所以栈就像是厨房里整齐的盘子摞,每次你只能用最上面的盘子,用完之后再把它放回去,这样就保持了整洁和有序。而在内存的栈中,数据也是这样整齐有序地存放和使用的。这就是为什么栈这么快,因为它不需要去搜索一个特定的盘子,它总是拿最上面的那个。

// 所以当我的程序执行到一个作用域的时候,就会往栈上去存放当前作用域有效的变量,在当前作用域没有使用,并且局部变量没有再被使用的时候,就会释放,这样理解对吗?
完全正确!就像你在玩积木,每进入一个新的游戏环节(也就是编程中的作用域),你就会在桌上(栈)搭建一些积木(变量)。只要在这个游戏环节中,你可以使用这些积木做很多事情。

当这个游戏环节结束了,也就是你完成了当前的积木搭建,这些积木就会被清理掉,桌面(栈)就又空出来了,可以用来进行下一个游戏环节的搭建。这样的好处是,你不用担心清理积木,因为游戏(程序)会自动帮你做这件事。

每次进入一个新的作用域,栈就会存放新的数据,当作用域结束,使用过的数据就会被移除。这个过程保证了内存的高效使用,因为你总是只在顶部搭积木,也只从顶部清理积木,这样操作起来非常快速。这也就是为什么栈被用来存储作用域内的局部变量,因为这些局部变量通常在作用域结束时就不再需要了。
  • 但是指针是存放在栈上的呀!

    fn main() {
        let s1 = String::from("hello");
        let _s2 = s1;
    
        println!("{}, world!", s1);
    }
    /**
    --> src/main.rs:5:28
      |
    2 |     let s1 = String::from("hello");
      |         -- move occurs because `s1` has type `String`, **which does not implement the `Copy` trait**
    3 |     let _s2 = s1;
      |               -- value moved here
    4 |
    5 |     println!("{}, world!", s1);
      |                            ^^ value borrowed here after move
      |
      = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
    help: consider cloning the value if the performance cost is acceptable
      |
    3 |     let _s2 = s1.clone();
      |                 ++++++++
    
    For more information about this error, try `rustc --explain E0382`.
    error: could not compile `ownership` (bin "ownership") due to previous error
    */
    
    • rust的浅拷贝会同时使原本的变量无效,这会避免重复的内存释放,因为rust在作用域结束时会自动调用drop方法。在上面那段代码中,当s1倍赋值给s2以后,就无效了,不能再被使用。Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。
      • 这里指的是存放在堆上的数据类型,只存在栈上的数据类型都是直接拷贝值。
      • 在栈上深浅拷贝,其实都一样,哈哈😅。
    • 函数和所有权
      • 把值传递给函数和赋值一样,都是传递出去所有权
    • 返回值和作用域
      • 如果函数在使用完值,没有将其返回出来,该值就会被清理,所以可以通过返回将所有权传递出去
    • 引用
      • 注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。

      • 几种引用类型

        • 不可变引用
          • 给予使用权,不能修改
        • 可变引用
          • 可以修改,但是同一时间只能存在一个可变引用

          • 其他引用都不能存在(包括可变、不可变引用)

            这就像是在家里,你不能同时打开音乐放得很大声,还让你的小猫在安静的房间睡觉,两件事情不能同时做。

            在Rust中,如果你创建了一个变量的可变引用,那么在这个引用存在的作用域内,你就不能再创建这个变量的不可变引用了。这是因为可变引用允许你改变变量的值,如果你还有一个不可变引用,那么在使用不可变引用读取变量值的同时,值可能会被可变引用改变,这样就会出现数据竞争。

        • 引用的生命周期
      • 关于悬浮引用的问题

        • rust不允许出现悬浮引用,可以看下方的例子,我们在
        fn main() {
            let reference_to_nothing = dangle();
        }
        
        fn dangle() -> &String {
            let s = String::from("hello");
        
            &s
        }
        
        • 在dangle函数作用域的末尾,变量s的空间会被释放,这时候就不能对它进行引用
        • 正确的做法应当是把所有权转交出去
      • 让我们概括一下之前对引用的讨论:

        • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
        • 引用必须总是有效的。
  • 字符串slice

    • rust允许我们对一个字符串创建一个部分字符的引用&str,这种引用和&String是一样的,当一个变量存在对它的slice引用时,它就不能够被修改
      • 首先,Rust 中的字符串是一位名门正派的高手,它有两种形态:String&strString 就像是一位自由行走江湖的侠客,它可以增长或减少,就像侠客的武功一样,随着经历的增加而成长。而 &str 呢,就是这位侠客的影子,它只是String的一部分,不过是个不能独立变化的存在。
      • 现在,当你通过 &str 来引用 String 的一部分时,就像是你派出了一个探子去监视这位侠客。因为探子在场,侠客就不能随便行动了(也就是不能被修改),因为这可能会让探子的报告失去准确性。
        • 那没有被盯着的另一部分字符串可以被修改吗
          • 在Rust世界里,我们得遵守的规璂之一是:当你有一个字符串String的引用&str时,无论是整个字符串还是字符串的一部分,都不能对原始的字符串进行修改。这就像是你不能在背后改变一位武林高手的故事,而他的影子正被人追踪一样,影子是不会撒谎的,它会一直忠实地反映原主体的状态。所以,哪怕你只关注(也就是只引用)这个字符串的一小段,整个String都会被锁定,直到那个引用结束。这是Rust中的借用检查器的作用,它确保了数据的完整性和并发访问的安全性。你可以想象成,只要有一个小小的窥视孔开着,整个房间都不能重新装修,因为那个小窥视孔给了任何人可能看到内部的权利。换句话说,不仅是那部分被引用的字符串不可以改变,整个原始字符串在有活跃引用时都是不可变的。这就是Rust中内存安全的哲学:任何事物只要被借出去观察,它就不应该在不知情的情况下发生变化。这避免了潜在的数据竞争和不一致状态,确保了程序运行时的安全和可靠。
      • 但这里有个小细节:&str&String 看起来是一样的,但实际上它们的出身稍有不同。&String 是对整个 String 侠客的引用,而 &str 可能只是对侠客的一部分技能的引用。不过,无论是引用整个还是部分,只要探子在场,侠客就得保持现状,不得私自改变。
      • 所以,总结一下,当你创建一个字符串的部分引用时,你基本上是在说:“嘿,这部分字符串,我得盯着你。” 而那部分字符串,或整个字符串,就被冻结了,直到你的引用结束,它才可以自由地变化。这就是为什么在 Rust 中处理字符串数据时,必须非常小心,以确保数据的安全性和有效性。
posted @ 2023-11-12 22:03  azoux  阅读(36)  评论(0编辑  收藏  举报