我的编程空间,编程开发者的网络收藏夹
学习永远不晚

详解Rust中的所有权机制

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

详解Rust中的所有权机制

Rust中的所有权机制

什么是所有权

Rust 的核心功能(之一)是 所有权(ownership)。虽然该功能很容易解释,但它对语言的其他部分有着深刻的影响。

所有程序都必须管理其运行时使用计算机内存的方式。

一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存——例如Java、Python等;在另一些语言中,程序员必须亲自分配和释放内存——例如C、C++等。

Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。

所以Rust具有安全性的原因之一是Rust的程序把大部分因为内存方面的安全问题在编译时给予扼杀。

所有权规则

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

String类型

其实这个已经在数据类型那一节的时候就应该简单介绍一下它,但是也没多大关系。String是一种结构体,其原型如下:

pub struct String {
    vec: Vec<u8>,
}

结构体这个概念对于有C语言基础的就不用多说了,前面还介绍过元组类型,元组其实就是更简单一点的结构体。

我们已经见过字符串字面值,即被硬编码进程序里的字符串值。

字符串字面值是很方便的,不过它们并不适合使用文本的每一种场景。原因之一就是它们是不可变的。另一个原因是并非所有字符串的值都能在编写代码时就知道:例如,要是想获取用户输入并存储该怎么办呢?

为此,Rust 有第二个字符串类型,String。这个类型管理被分配到上的数据,所以能够存储在编译时未知大小的文本。可以使用 from 函数基于字符串字面值来创建 String,如下:

let s = String::from("hello");

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

关于String的一些简单的操作:

往String类型的变量尾部插入字符和字符串分别可以使用push和push_str函数完成。

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

结果:

s = hello, world!

因为String对 ‘+’ 做了运算符重载,所以上面的操作也可使用 '+'完成:

fn main() {
    let mut s = String::from("hello");
    s = s + ",";	//这里是双引号
    s = s + " world!";
    println!("s = {}", s);
}

String和&str的区别在于两个类型对内存的处理上。

内存与分配

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

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

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

就是字符串字面量的内存是在栈上,而String类型的内存是在堆上。

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

  • 有GC的语言:不需要关心不再使用的内存,因为会自动GC.
  • 没有GC的语言:一次申请内存对应一次释放内存。

Rust的处理方式:内存在拥有它的变量离开作用域后就被自动释放。

 {
        let s = String::from("hello"); // 从此处起,s 是有效的

        // 使用 s
    }                                  // 此作用域已结束,
                                       // s 不再有效

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

说白了Rust的机制就是对应着C++的智能指针中unique_ptr的机制。

移动

在Rust 中,多个变量可以采取不同的方式与同一数据进行交互。

例如:

  • 标量的版本
let x = 5;
let y = x;

因为整数是有已知固定大小的简单值,所以这两个 5 被放入了栈中。

原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它。

  • String类型的版本
let s1 = String::from("hello");
let s2 = s1;

String 由三部分组成:

  • 指向存放字符串内容内存的指针
  • 长度
  • 容量。

这一组数据存储在栈上。String通过其内部的成员指针,访问到实际字符串的位置。如下图所示:

在这里插入图片描述

在Rust中如果出现以下的情况时:

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

那么Rust采取的方式是,把s1的所有权移交给s2,那么此后s1不不可再对原来的内存进行操作(保证Rust中的所有权的规则)。

在这里插入图片描述

另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。

克隆

因为Rust不会自动复制变量中的具体内容,而有些场景中,我们有希望拷贝原来的内容,那Rust也给我们提供了一种克隆的方式,这样就符合我们原来的编码习惯。

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

println!("s1 = {}, s2 = {}", s1, s2);

这样的话就没有将s1的所有权移交给s2,而是s2拷贝了一份新的s1的内容。

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

任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。如下是一些 Copy 的类型:

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

所有权与函数

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

例如:

fn main() {
    let i = 9;
    fun(i);
    //程序走到这里时i依然有效
    let s = String::from("hello");
    fun2(s);
    //程序走到这里时s不再有效
}
//在参数传进来的时候实际上是发生了 形参 = 实参 的事情
fn fun(x: i32) {	
    ...
}
fn fun2(y: String) {
    ...
}

上述例子就相当于发生了以下的操作:

let i = 9;
let x = i;	//这里是发生了Copy,不会发生所有权的转移
let s = String::from("hello");
let y = s;	//这里因为String类型是在堆上申请内存,所以发生了所有权的转移

如果想要使s调用完函数(移交所有权后),还能再次使用则需要把所有权移交回来,例如以下例子:

fn main() {
    let mut s = String::from("hello");
    s = show_string(s);
}
fn show_string(x: String) -> String{
    println!("{}", x);
    x
}

引用与借用

因为移交所有权后再移交回来这种方式太笨了,所以Rust提供了一种引用的方式方便我们操作。

引用(reference)像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。 与指针不同,引用确保指向某个特定类型的有效值。

简单来讲就是引用就是有一种不好的感觉,我让你帮我办一件事,你办事的工具从头到尾都是你的,我从来就没碰过,你只需要帮我把事情办了就好。

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

    let len = calculate_length(&s1);

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

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

注意我们传递 &s1calculate_length,同时在函数定义中,我们获取 &String 而不是 String。这些 & 符号就是 引用,它们允许你使用值但不获取其所有权。

把上述calculate_length函数所表达的翻译成“不好的”的方式表达:

calculate_length时期,s是一个办事不露面的人,他计划着要办len这件事,于是他让s1帮他完成他愿望,因为s1擅长处理len这件事。

在这里插入图片描述

变量 s 有效的作用域与函数参数的作用域一样,不过当 s 停止使用时并不丢弃引用指向的数据,因为 s 并没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。

我们将创建一个引用的行为称为 借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。我们并不拥有它。

可变引用

因为在Rust中,变量默认都是不可变的,引用也是变量,所以当我们需要修改内存中的内容的话需要加上mut关键字才可以进行修改。

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

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。这些尝试创建两个 s 的可变引用的代码会失败:

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> class="lazy" data-src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 | 
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error

这个报错说这段代码是无效的,因为我们不能在同一时间多次将 s 作为可变变量借用。

这一限制以一种非常小心谨慎的方式允许可变性,防止同一时间对同一数据存在多个可变引用。新 Rustacean 们经常难以适应这一点,因为大部分语言中变量任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;

Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!

允许拥有多个可变引用,只是不能 同时 拥有:

 let mut s = String::from("hello");
    {
        let r1 = &mut s;
    } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

    let r2 = &mut s;
fn main() {
    let mut s = String::from("hello");
    test(&mut s);

    let r2 = &mut s;
    println!("r2 = {}", r2);
}

fn test(x: &mut String) {
    x.push_str("world");
}
r2 = helloworld

Rust 在同时使用可变与不可变引用时也采用的类似的规则。

 let mut s = String::from("hello");

    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    let r3 = &mut s; // 大问题

    println!("{}, {}, and {}", r1, r2, r3);
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> class="lazy" data-src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 | 
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

不能在拥有不可变引用的同时拥有可变引用。多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。

错误版本:

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s; //有问题,因为下面还在使用r1,r2
    println!("{},{}", r1, r2); 
    println!("{}", r3);
}

正确版本:

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{},{}", r1, r2);
    // 此位置之后 r1 和 r2 不再使用
    let r3 = &mut s;	//没问题,因为后面不再使用r1,r2
    println!("{}", r3);
}

悬垂引用

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

悬垂引用就是野指针的意思,有C/C++基础的就不用多说了。

fn main() {
    let reference_to_nothing = dangle();	//访问到了已经被释放内存的地址,野指针
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s	//返回s所指向的地址
}	//s离开其作用域,则s所指向的内存被释放
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> class="lazy" data-src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error

像以下的代码是没有错的:

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

因为返回的不是引用,所以就相当于把s的所有权移交给一个String类型的无名对象,然后在函数调用那块又把这个无名对象的所有权移交给接收者。

引用的规则

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

Slice类型(切片)

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

字符串切片

因为slice是一类部分引用,所以字符串切片就是原来字符串的一部分。

    let s = String::from("hello world");
	//0..5 左闭右开 [0,5)	左闭右闭 0..=5 [0,5]
    let hello = &s[0..5];	//hello
    let world = &s[6..11];	//world
  • 在Range(范围)中,如果左是0,则可以简写0
  • 在Range(范围)中,如果右是字符串长度(字符串尾部),则可以简写
  • 如果以上两个都满足,则左和右都可以简写
let s = String::from("hello");
let slice = &s[0..2];	//he
let slice = &s[..2];	//he
let len = s.len();

let slice = &s[3..len];	//lo
let slice = &s[3..];	//lo

注意:字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出。出于介绍字符串 slice 的目的,本部分假设只使用 ASCII 字符集;第八章的 “使用字符串存储 UTF-8 编码的文本” 部分会更加全面的讨论 UTF-8 处理问题。

字符串字面值被储存在二进制文件中。

let s = "Hello, world!";

这里 s 的类型是 &str:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str 是一个不可变引用。

其他类型的 slice

以整型数组为例,当然其他的类型也都是类似的。

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];	//[2, 3]

assert_eq!(slice, &[2, 3]);

总结

所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。

Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。

到此这篇关于Rust中的所有权机制的文章就介绍到这了,更多相关Rust所有权机制内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

详解Rust中的所有权机制

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

详解Rust中的所有权机制

Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码,这篇文章主要介绍了Rust中的所有权机制,需要的朋友可以参考下
2022-11-13

Rust 编程语言中的所有权ownership详解

这篇文章主要介绍了Rust 编程语言中的所有权ownership详解的相关资料,需要的朋友可以参考下
2023-02-16

详解Node.js中的事件机制

前言 在前端编程中,事件的应用十分广泛,DOM上的各种事件。在Ajax大规模应用之后,异步请求更得到广泛的认同,而Ajax亦是基于事件机制的。 通常js给我们的第一印象就是运行在客户端浏览器上面的脚本,通过node.js我们可以在服务端运行
2022-06-04

Pytorch中的广播机制详解(Broadcast)

这篇文章主要介绍了Pytorch中的广播机制详解(Broadcast),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
2023-01-03

详解bash中的初始化机制

Bash初始化文件 交互式login shell在下列情况下,我们可以获得一个login shell:uFJHrKxWJu 登录系统时获得的顶层shell,无论是通过本地终端登录,还是通过网络ssh登录。这种情况下获得的login she
2022-06-04

node.js中的事件处理机制详解

EventEmitter类 在Node.js的用于实现各种事件处理的event模块中,定义了一个EventEmitter类。所有可能触发事件的对象都是一个集成了EventEmitter类的子类的实例对象,在Node.js中,为EventEm
2022-06-04

java 中复合机制的实例详解

java 中复合机制的实例详解继承的缺陷继承的缺陷是由它过于强大的功能所导致的。继承使得子类依赖于超类的实现,从这一点来说,就不符合封装的原则。一旦超类随着版本的发布而有所变化,子类就有可能遭到破坏,即使它的代码完全没有改变。为了说明的更加
2023-05-31

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录