Day 02 - 所有权

本贴最后更新于 395 天前,其中的信息可能已经渤澥桑田

image

所有权入门

Rust 的核心特性就是所有权,所有计算机语言都必须管理他们使用计算机内存的方式:

  • 类似 Java 的 他会在运行时中 不断地寻找不再使用的内存进行 CG
  • 类似 C/C++ 的 它会显示的让程序员进行内存的释放与分配(C++ 只是部分版本)

Stack Heap

栈内存和堆内存都是我们常用的内存但是他们的结构麻~不同

Stack 和 Heap 的存储数据

Stack 顾名思义 LIFO 的数据结构 添加数据叫压栈 弹出数据叫弹栈。所有存放在栈上的数据必须是已知大小的并且拥有固定的大小的,所以我们在编译大小未知的数据的时候会考虑将他放在 Heap 中。

当你把数据放在 Heap 中你会请求一定数量的空间,OS 会在 Heap 中找到一块足够大小的空间标记为在使用,并返回指向这个空间的指针(内存地址),这种过程呢我们叫做内存空间分配。

压栈存储比我们的分配存储时间要快很多,因为 OS 不需要再压栈的时候去找给变量放置的空间,而是直接找到顶针,然后进行压栈就可以了。

image

Stack 和 Heap 的访问数据

image

所有权的职能

跟踪代码的哪些部分再使用 Heap 的哪些数据

最小化 Heap 的数据量并清理 Heap 上未使用的数据避免空间不足。

所有权规则

变量作用域

fn main() {
    // s不可用
    let s = 2; // s可用 入栈
    // s 可用
} // 退出了s的作用域 s不可用 弹栈

接下来我们先了解下 String 类型

一般的基础数据类型都是放在栈上面的而不是堆上面的,我们的 String 可以放在堆上面

创建一个字符串

fn main() {
    let mut s = String::from("hello");
    s.push_str(",World");
    println!("{s}");
}

String 为了支持可变性(区别于上述代码的"hello"之类的字面值)需要在 Heap 中保存编译时未知的文本内容

用完 String 之后需要使用某种方式将内存返回给操作系统 :

如果返回了多次和没有返回还有提前返回都会造成 Bug、浪费内存和无法使用变量的问题,Rust 中某个变量走出自己的作用域的时候会默认调用一个 drop 方法将变量的内存交给操作系统。

移动

普通变量的移动据我们所知 是进行压栈加入的内存

我们这里讨论的主要是类似 String 的变量

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

一个 String 类型有三部分:

  • 指向字符串内容的内存指针 ptr
  • 长度 len 存放字符串内容所需的字节数
  • 容量 capacity 操作系统上总共获得内存的总字节数

上面这些东西都是 Stack 的 而字符串的内容是放在 heap 中的

当 s 的值赋值给 s2 的时候,s2 只是再栈内存中压栈了一个新的元素里面有 ptr len 和 capacity ptr 同样的指向我们 s 的堆内存地址中。

image

当我们的 s1 和 s2 都离开作用域的时候,他们都会进行释放,但是因为堆内存的数据只有一个空间,s s2 释放了两次导致出现 Bug:​ 二次释放,Rust 为了防止这样的事情发生索性 S 给 S2 赋值之后 S 自动失效,

fn main() {
    let mut s = String::from("hello");
    let mut s2 = s;
    print!("{}", s);
}

报错 因为 s 再把值给 s2 之后 会失效, 我们将之称之为 s 移动到了 s2, 移动和我们的浅拷贝类似 不过浅拷贝会使得所有进行拷贝的变量不失效.

克隆

fn main() {
    let mut s = String::from("hello");
    let mut s2 = s.clone();
    print!("{}", s);
}

克隆会把堆上面的数据复制一份交给我们的新数据

复制

Rust 有这么两个特性: copy trait 和 drop trait(trait 这东西他类似与 interface)

当一个数据实现了 copy trait 那么他再把值交给一个新的变量的时候 是不会自动失效的(也就是旧的值任然可以使用) 一个类型实现了 DropTrait 后就不能实现 CopyTrait 了。

image

这就是所有权的三个规则:复制 克隆 移动

函数与所有权

当把一个变量传给函数的参数时,可以看作这个变量移动给了函数,所以下列的代码时不能执行的:

fn main() {
    let mut s = String::from("hello");
    do_thing(s);
    print!("public -> {s}"); // 不存在s了 因为再执行参数的使用发生了移动 ->报错
  
    let mut num = 2;
    do_thing_int(num);
    print!("{num}"); // 可以 因为number时放在栈里面的
}

fn do_thing_int(int:i32) -> i32{
    print!("{int}");
    int
}

fn do_thing(str: String) -> String {
    print!("{}", str);
    str
}

总结:

  • 以下情况会发生转移

    • 把一个值赋值给其他变量时就会发生移动
    • 当包含 heap 数据的变量离开作用域的时候会被 drop 除非他移动到了另一个变量上

为了让我们变量不再因为传参的时候移动到方法中导致变量被销毁,我们可以再执行方法的时候把变量返回回去

fn main() {
    let a = String::from("你好,我是刘");
    let a = do_thing(a);
    print!("{a}");
}


fn do_thing(str: String) -> String {
    print!("{}", str);
    str
}

像极了乱跑的小孩子跑到别人家里捣乱,然后大人无赖的将他送了回来...

引用

引用允许你引用某些值而不是取得其所有权

fn main() {
    let a = String::from("你好,我是刘");
    do_thing(&a);
    print!("{a}");
}


fn do_thing(str: &String)  {
    print!("{}", str);
}

把引用传给参数就是借用

我们可以再函数里面使用变量名来获取引用的变量这就是我们的自动解引用,和变量一样默认的借用也是不可变的。

fn main() {
    let mut a = String::from("你好,我是刘"); // 变量是可变的
    do_thing(&mut a); // 传入的参数时可变的
    print!("{a}");
}


fn do_thing(str: &mut String)  { // 传入的参数类型时可变的引用	
    str.push_str("你好"); // 自动解引用
    print!("{}", str);
}

限制:我们的引用有一个限制就是在同一个作用域下某个变量的引用只能存在一个,因为这样会避免数据的竞争。

fn main() {
    let mut a = String::from("你好,我是刘");
    let mut b = String::from("你好,我是章");
    let mut ay = &mut a;
    let mut by = &mut a;
    do_thing(ay); // 报错 因为 a的可变引用只能存在一个
}


fn do_thing(str: &mut String)  {
    str.push_str("你好");
    print!("{}", str);
}

还有就是一个作用域下不能存在同一个变量的一个可变的引用和一个不可变的引用,因为可变引用一旦改变了引用的值,不可变引用的效果就失去了...

可以同时读 但是不可以同时写 也不可以一个写一个读

&str 是切片的意思 你可以看看下一章的切片 温馨提示 push_str 有用到 a 的可写引用哦

fn main() {
    let mut a = String::from("Hell o World");
    let b = get_char(&mut a); // 有个可写的 返回一个可读的 所以报错
    a.push_str("123");
    println!("{b}");
}

fn get_char(s: &String) -> &str {
    let chars = s.as_bytes();

    for (index, &value) in chars.iter().enumerate() {
        if value == b' ' {
            return &s[..index];
        }
    }
    return &s[..];
}

慢慢想为什么这个代码无法被执行

fn main() {
    let mut a = String::from("Hell o World");
    let b = get_char(&mut a);
    a.push_str("123");
    println!("{b}");
}

fn get_char(s: &mut String) -> &mut str {
    let chars = s.as_bytes();

    for (index, &value) in chars.iter().enumerate() {
        if value == b' ' {
            return &mut s[..index];
        }
    }
    return &mut s[..];

}

切片

fn main() {
    let a = String::from("HelloWorld");
    let b = &a[0..3];
    println!("{b}");

    let b = &a[3..];
    println!("{b}");

    let b = &a[..3];
    println!("{b}");
}

字符串字面值其实也是一个切片image &str 表示的就是一个切片所以他是不可变的

我们之前的代码是

fn get_char(s: &String) -> &str

这样只能传入字符串到函数中,我们的字面值似乎就无法传进来(因为字符串的字面值不是字符串而是切片 -字符串不是字符串~-)

但是我们可以这么理解 切片是字符串

fn get_char(s: &str) -> &str

所以我们之前的代码可以这么写

fn main() {
    let mut a = String::from("Hell o World");
    let ret = {
        let b = get_char(&mut a); // 我们在这里拿到了一个 可写切片的引用
        println!("{}", b);
        b // 我们这里单纯的将切片交给外部
    };
    a.push_str("123"); // 这里我们进行另外一个可写变量的使用
}

fn get_char(s: &mut str) -> &mut str {
    let first_word_len = s.find(' ').unwrap_or_else(|| s.len());
    let first_word = &mut s[..first_word_len];
    first_word
}

  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    57 引用 • 22 回帖 • 5 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...