学习笔记
Git
  • C#
  • F#
  • AspNetCore
  • EF Core
  • WPF
  • Tauri
  • IoTDB
  • Redis
  • Docker
  • Kubernetes
  • RabbitMQ
  • 日志

    • NLog的配置文件
Linux
基础设施
  • Vue

    • Vue基础
VS Code的插件与玩法
Rust
Python
  • Maven
  • Spring Boot
实际项目
我的博客
Git
  • C#
  • F#
  • AspNetCore
  • EF Core
  • WPF
  • Tauri
  • IoTDB
  • Redis
  • Docker
  • Kubernetes
  • RabbitMQ
  • 日志

    • NLog的配置文件
Linux
基础设施
  • Vue

    • Vue基础
VS Code的插件与玩法
Rust
Python
  • Maven
  • Spring Boot
实际项目
我的博客
  • Rust中的变量
  • 数据类型
  • 函数
  • 控制流
  • 所有权
  • 结构体

所有权

所有权是Rust中一个重要概念,在其它编程语言中并不常见,所有权让Rust无需垃圾回收(GC)也能保证内存安全

所有权的主要目的是管理堆数据

所有权规则:

  1. Rust 中的每一个值都有一个 所有者(owner)。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

堆上的数据怎么释放?

栈上的数据处理是简单的,将值复制一份,然后在变量的生命周期结束后释放掉就好了

但是堆上的数据就不是那么简单了,大多数编程语言使用堆上的数据都遵循这样一种结构:先在栈上保存这个变量对应的数据在堆上的指针(根据不同的类型还有其他的参数),然后通过这个指针去寻找到堆上的数据,Rust也使用了这种模式

变量与数据交互的方式(一):移动

let s1 = String::from("hello");
let s2 = s1;

// 会抛异常,在 let s2 = s1; 之后,Rust 认为 s1 不再有效
println!("{s1}, world!");

在Rust中,声明了s2后,s1就无效了。有GC的语言中,在声明s2 = s1后,s2和s1都指向了相同的堆内存地址,当s1和s2变量的生命周期都结束后,由GC去释放所引用的堆内存;C/C++则是由手动free,这样很容易遗漏,或者重复释放。Rust的特点就是无GC又不需要手动释放,于是选择了通过所有的权的概念,去自动free

一想到要自动free,那么最好的时机就是在变量的生命周期结束的时候去free,如果这个堆中的内存对象只有一个引用就很简单了,但是向上文这样声明了s2 = s1的时候,情况就复杂起来了,当s1生命周期结束需不需要free堆内存?当s2生命周期结束需不需要free堆内存?Rust给的答案是,最后声明引用的变量生命周期结束后free堆内存。解决了这个问题后又出现了新的问题,最后声明引用的变量生命周期不一定比先声明的变量生命周期长,于是Rust规定当使用一个新变量引用旧变量后,旧变量就失效。这么约定的原因是Rust认为如果你要在一个长生命周期内使用一个短生命周期的变量去引用它,不如直接使用这个长生命周期的变量

变量与数据交互的方式(二):克隆

let s1 = String::from("hello");
let s2 = s1.clone();

// 正常输出
println!("s1 = {s1}, s2 = {s2}");

如果程序中s1和s2都需要使用呢?可以显示使用clone方法,需要注意的是,这时候s2和s1并非引用同一个堆内存地址,而是将s1的堆内存地址复制了一份,也就是常说的“深拷贝”

函数调用对生命周期的影响

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,
                                    // 所以在后面可继续使用 x

} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 没有特殊之处

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。
  // 占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊之处

上面谈到了移动和克隆,这都是针对堆上的数据,那么栈上的数据呢?Rust和其他编程语言一样,基本数据类型在栈上(也有在栈上的自定义类型),当传递在栈上的变量时,会自动传递clone后的变量。

如果有C#或者Java之类编程语言的经验,那么可以简单的看,如果在C#或者Java的函数中修改形参的值,会一同修改到实参的值,那么在Rust中,就会发生移动,也就是引用类型(C#中的引用类型和值类型的概念)会触发移动,触发移动后原来的变量就失效了

除了传递给函数参数会触发移动,将值从函数中返回也会触发移动,如果没有变量接收返回值,那么就结束生命周期free掉堆上的内存,如果有变量接收,那么就移交所有权给新的变量

引用

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

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

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

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

    (s, length)
}

还是从一段代码来看,上面讲到了通过参数会触发移动,那么想要一个变量在调用函数后依然可用,那就只有通过返回值触发移动将所有权再移交回来,但是这样就显得很呆。

为了解决这个很呆的用法,Rust提出了引用的概念

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

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

这里calculate_length的参数类型从String变为了&String,这时候calculate_length所需要的类型变成了String类型的引用,而在调用这个函数传递参数时,也添加了&符号let len = calculate_length(&s1);

当传递一个变量的引用时,并不会发生所有权的改变,因为所有权没有发生改变,所以calculate_length函数并不能修改对应值,Rust中的修改必须要有所有权

创建一个引用的行为叫做借用,借用是一个行为上的名称

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

Slice

slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一种引用,所以它没有所有权。

Last Updated:
Contributors: 吴俊城
Prev
控制流
Next
结构体