写点什么

30 天拿下 Rust 之所有权

作者:希望睿智
  • 2024-05-25
    安徽
  • 本文字数:2706 字

    阅读完需:约 9 分钟

30天拿下Rust之所有权

概述

在编程语言的世界中,Rust 凭借其独特的所有权机制脱颖而出,为开发者提供了一种新颖而强大的工具来防止内存错误。这一特性不仅确保了代码的安全性,还极大地提升了程序的性能。在 Rust 中,所有权是一种编译时检查机制,用于追踪哪些内存或资源何时可以被释放。每当一个变量被赋予一个值(比如:字符串、数组或文件句柄)时,Rust 会确定这个变量是否“拥有”这个值,拥有资源的变量负责在适当的时候释放这些资源。

所有权的规则

在 Rust 中,每个值都有一个被称为“所有者”的变量。同一时间内,这个值只能有一个所有者,并且当所有者(变量)离开作用域时,该值会被自动释放,不需要我们手动释放,这就是所谓的“所有权”。这意味着 Rust 通过编译期检查,强制执行资源生命周期管理,从根本上杜绝了内存泄漏问题。

Rust 的所有权规则非常简单,只有以下三条,但却非常有效。

1、单一所有者:在任何给定的时间,只有一个变量可以拥有某个资源。这确保了不会出现数据竞争,因为只有一个所有者可以修改或释放资源。

2、移动语义:当资源从一个变量转移到另一个变量时,所有权也随之移动。这意味着原始变量不再拥有资源,新变量现在负责释放资源。这种转移是通过“移动”操作来完成的,这类似于 C++ 11 中的移动语义。

3、释放资源:当拥有资源的变量离开其作用域时,Rust 会自动释放该资源。这确保了不会发生内存泄漏,因为资源总是在不再需要时被清理。

栈和堆

在 Rust 中,值是位于栈上还是堆上,在很大程度上影响了语言的行为。因此,在继续介绍下面的内容之前,我们有必要先学习下栈和堆的知识。

当一个函数被调用时,它的局部变量和参数通常会被分配在栈上。当函数执行完毕返回时,这些变量会自动被清理。栈内存的访问速度非常快,因为栈具有连续的内存空间,CPU 可以直接通过指针运算访问栈上的数据。但栈的大小通常是有限制的,因为栈是后进先出的数据结构。如果递归调用过深或者分配了过多的局部变量,可能会导致栈溢出。

堆内存由程序员(或编程语言运行时)手动分配和释放。在 Rust 中,使用 String、Vec 等数据时,数据通常会被分配在堆上。由于堆内存是分散的,访问堆上的数据通常比访问栈上的数据要慢。堆的大小通常比栈大得多,并且没有严格的后进先出限制,这使得堆适合存储生命周期不确定或需要大量内存的数据。

移动和克隆

在 Rust 中,数据的移动和克隆是处理数据所有权和交互的两种非常重要的机制。

对于栈上的数据,赋值时,数据是直接克隆或拷贝的,不涉及移动的概念。一些基本数据类型(包括:整型、浮点型、布尔型、字符型、仅包含以上类型的元组)对应的变量不需要存储到堆上,都是存储到栈上的。

fn main() {    let x = 5;    let y = x;    // 栈上的数据,赋值时进行克隆    println!("{0} {1}", x, y);}
复制代码

对于堆上的数据,赋值时,默认是进行移动的。当数据通过值传递时,会发生数据的移动。这意味着数据的所有权会从发送方转移到接收方。一旦数据被移动,原始数据就不再有效,因为它不再拥有数据的所有权。

fn main() {    let str1 = String::from("Hello, World");    // str1的所有权会移动到str2    let str2 = str1;      // 会提示编译错误:value borrowed here after move    // println!("str1: {}", str1);
// str2现在拥有所有权 println!("str2: {}", str2);}
复制代码

在上面的示例代码中,str1 的所有权被移动到了 str2,因此 str1 不再有效。如果我们尝试使用 str1,Rust 编译器会报错。

对于堆上的数据,如果我们既想要保留原始数据的所有权,又想让另一个变量拥有相同的数据,可以使用 clone 方法来创建数据的一个副本。在 Rust 中,不是所有的类型都实现了 Clone 特征,但对于那些实现了 Clone 的类型(比如:String、Vec 等),我们可以调用 clone 方法来创建一个新的副本。

fn main() {    let str1 = String::from("Hello, World");    // 创建str1的副本,而不是移动所有权    let str2 = str1.clone();      // str1仍然拥有所有权    println!("str1: {}", str1);
// str2拥有str1的副本 println!("str2: {}", str2);}
复制代码

在上面的示例代码中,str1.clone() 创建了 str1 的一个副本,并将所有权赋给了 str2。这样,str1 和 str2 都拥有有效的数据,并且都可以独立地使用。

注意:clone 方法通常涉及到数据的深拷贝,这可能会消耗额外的内存和性能。因此,在需要频繁复制大型数据结构时,应该考虑其他策略,比如:使用引用或智能指针来共享所有权。

所有权的使用

在 Rust 中,函数与所有权的关系是紧密相联的。函数涉及的所有权主要有两种:一种是函数参数的所有权,另一种是函数返回值的所有权。

1、函数参数的所有权。当你通过值传递一个变量给函数时,该变量的所有权会转移到函数中。函数内部可以自由地修改和使用这个变量,而原始变量在函数调用后将不再有效。这种所有权转移,确保了数据在函数中的安全性和一致性。

struct Data {    value: i32,}
fn process_data(data: Data) { // data获得了所有权 println!("{}", data.value); // 函数结束时,data的所有权会被释放}
fn main() { let cur_data = Data { value: 66 }; // 将cur_data的所有权传递给process_data函数 process_data(cur_data); // my_data的所有权已经被转移,故下面的代码会提示编译错误 // println!("{}", cur_data.value);}
复制代码

在上面的示例代码中,我们定义了一个名为 Data 的结构体,它包含一个 i32 类型的字段。当我们把这个结构体变量 cur_data 作为参数传递给 process_data 函数时,cur_data 的所有权被转移到了函数的参数 data 中。因此,在 process_data 函数执行期间,data 可以被自由地使用。但一旦函数执行完毕,cur_data 的所有权就被释放了,因此我们不能在后面再次访问它,否则会导致编译错误。

2、函数返回值的所有权。函数可以返回值,而返回值的所有权会转移到调用方。这意味着,调用方负责该值的生命周期。

fn greet(name: String) -> String {    let text = format!("Hello, {}", name);    // 当函数返回text时,它的所有权将被转移到调用方    return text;}
fn main() { // 创建一个String,并将其所有权传递给greet函数 let name = String::from("World");
// 调用greet函数,并获得返回值的所有权 let result = greet(name); println!("{}", result);}
复制代码

总结

Rust 的所有权模型是一种独特而强大的工具,也是一套严谨而灵活的编程范式。它确保了内存安全,简化了并发编程,并赋予了开发者更高的控制力,使他们能够编写出既安全又高效的软件。这是 Rust 区别于其他现代编程语言的独特魅力所在,也是其在系统级编程、网络服务、嵌入式开发等各个领域大放异彩的重要原因。

发布于: 刚刚阅读数: 4
用户头像

希望睿智

关注

一起学习,一起成长,一起进步! 2024-05-21 加入

中国科学技术大学毕业,在客户端、运营级平台、Web开发、嵌入式开发、深度学习、人工智能、音视频编解码、图像处理、流媒体等多个领域具备实战开发经验和技术积累,共发表发明专利十余项,软件著作权几十项。

评论

发布
暂无评论
30天拿下Rust之所有权_rust语言_希望睿智_InfoQ写作社区