Rust核心功能之一(所有权)

目录


1、什么是所有权?

所有权(系统)是 Rust 最为与众不同的特性,对语言的其他部分有着深刻含义。它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全,因此理解 Rust 中所有权如何工作是十分重要的。

所有程序都必须管理其运行时使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。

因为所有权对很多程序员来说都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。

1.1 所有权规则

首先,让我们看一下所有权的规则。当我们通过举例说明时,请谨记这些规则:

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

 1.2 变量作用域

在所有权的第一个例子中,我们看看一些变量的 作用域scope)。作用域是一个项(item)在程序中有效的范围。假设有这样一个变量:

let a = 12345

变量 a绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前 作用域 结束时都是有效的。

fn main() {                // 作用域开始处
   let a = 12345;          // 此时,a才开始分配存储空间  
}                          // 作用域开始处

其实变量是否有效与作用域的关系跟其他编程语言是类似的。

1.3 String 类型

我们已经见过字符串字面值,即被硬编码进程序里的字符串值。字符串字面值是很方便的,不过它们并不适合使用文本的每一种场景。原因之一就是它们是不可变的。另一个原因是并非所有字符串的值都能在编写代码时就知道:例如,要是想获取用户输入并存储该怎么办呢?为此,Rust 有第二个字符串类型,String。这个类型管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本。可以使用 from 函数基于字符串字面值来创建 String,如下:

fn main() {                
    let s = String::from("hello");
    println!("{s}")
}   

这两个冒号 :: 是运算符,允许将特定的 from 函数置于 String 类型的命名空间(namespace)下,而不需要使用类似 string_from 这样的名字。

可以修改此类字符串可变:

fn main() {                
    let mut s = String::from("hello ");
    s.push('w');
    s.push_str("orld");
    println!("{s}")
}                          

那么这里有什么区别呢?为什么 String 可变而字面值却不行呢?区别在于两个类型对内存的处理上。

1.4 内存与分配

就字符串字面值来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件中。这使得字符串字面值快速且高效。不过这些特性都只得益于字符串字面值的不可变性。不幸的是,我们不能为了每一个在编译时大小未知的文本而将一块内存放入二进制文件中,并且它的大小还可能随着程序运行而改变。

对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:

  • 必须在运行时向内存分配器(memory allocator)请求内存。
  • 需要一个当我们处理完 String 时将内存返回给分配器的方法。

第一部分由我们完成:当调用 String::from 时,它的实现 (implementation) 请求其所需的内存。这在编程语言中是非常通用的。

然而,第二部分实现起来就各有区别了。在有 垃圾回收garbage collectorGC)的语言中,GC 记录并清除不再使用的内存,而我们并不需要关心它。在大部分没有 GC 的语言中,识别出不再使用的内存并调用代码显式释放就是我们的责任了,跟请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要精确的为一个 allocate 配对一个 free

Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。

这是一个将 String 需要的内存返回给分配器的很自然的位置:当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop

注意:在 C++ 中,这种 item 在生命周期结束时释放资源的模式有时被称作 资源获取即初始化Resource Acquisition Is Initialization (RAII))。如果你使用过 RAII 模式的话应该对 Rust 的 drop 函数并不陌生。

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

fn main() {
    let x = 5;
    let y = x;
}

定义的2个简单变量,会直接存储在栈中,因为它们的长度都是固定的。

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

当s1、s2存储的时候,会在堆中开辟一片区域用来存储,s1和s2 则通过指针的方式,来指向堆中存储值得位置,这样可以避免在堆中多次开辟存储空间,如果你在其他语言中听说过术语 浅拷贝shallow copy)和 深拷贝deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动move),而不是叫做浅拷贝。上面的例子可以解读为 s1 被 移动 到了 s2 中。如果打印s1会出现以下错误:

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

 如果我们 确实 需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。

以下是一个示例:

fn main() {                
    let s1 = String::from("hello ");
    let s2 = s1.clone();
    println!("{s1} {s2}")
}       

打印以下看下结果:

只在栈上的数据:拷贝

fn main() {
    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);
}

 Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。

那么哪些类型实现了Copy Trait呢,有以下类型:

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 true 和 false
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

1.5 所有权与函数

将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或者复制,就像赋值语句一样。

一个示例:

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 移出作用域。没有特殊之处

可以发现在值传递给函数的时候,堆里的数据会移除当前作用域,例如,s传递takes_ownership方法里,在当前作用域s的值被释放,栈里的值,传递给函数,它对应的操作是复制,例如,x=5传递给函数之后,在当前作用域还是可以访问的。

1.6 返回值与作用域

返回值也可以转移所有权。以下是一个示例:

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 转移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 离开作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 会将
                                             // 返回值移动给
                                             // 调用它的函数

    let some_string = String::from("yours"); // some_string 进入作用域。

    some_string                              // 返回 some_string 
                                             // 并移出给调用的函数
                                             // 
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
                                                      // 

    a_string  // 返回 a_string 并移出给调用的函数
}

这里跟上面移动规则一样,将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

以上我们都是通过一个变量来获取所有,这样来回赋值就会有些啰嗦,这样的话,我们可以使用元组来返回多个值。

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 对此提供了一个不用获取所有权就可以使用值的功能,叫做 引用references),后面我们再来看看引用是什么?

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年12月5日
下一篇 2023年12月5日

相关推荐