首页 > 其他分享 >rust语言之所有权

rust语言之所有权

时间:2024-08-22 19:38:27浏览次数:5  
标签:生命周期 语言 语义 参数 引用 借用 所有权 rust

Rust受现代c++的启发,引入智能指针来管理堆内存。在rust中,所有权是系统性的概念,是rust语言的基础设施。

5.1 通用概念

编程语言中的值主要分成两类:

  • 值类型(Value):数据直接存储在栈中的数据类型
  • 引用类型(Reference):将数据存在堆中,而栈中值存放指向堆中数据的地址(指针)

为了更精确的对复杂类型进行描述,引入值语义和引用语义:

  • 值语义(Value Semantic):按位复制以后,和原始对象无关
  • 引用语义(Reference Semantic):也叫指针语义。一般是指将数据存储于对内存中,通过栈内存的指针来管理堆内存的数据,并且引用语义禁止按位复制。

按位复制就是指栈复制,也叫浅复制,只复制栈上的数据。深复制就是对栈上和堆上的数据一起复制。

对于实现Copy trait的类型,其 clone 方法必须是按位复制的。

rust通过 Copy 这个标记 trait 将类型按值语义和引用语义做了精准的分类,帮助编译期检测出潜在的内存安全问题。

引用语义不能实现copy,但是可以实现 Clone 的 clone 方法,以实现深度复制,在需要时可以显式调用。

5.2 所有权机制

在所有权体制下,Rust引入新的语义:

  • 复制(copy):对应值语义。对于可以安全在栈上进行按位复制的类型,就只需要按位复制
  • 移动(move):对应引用语义。对于在堆上存储的数据,无法安全地在栈上进行按位复制。如果堆上的数据不变,只需要在栈上移动指向堆内存的指针地址,不仅保证了内存安全,也拥有与栈复制同样的性能。

一个值的所有权被转移给另外一个变量绑定的过程,就叫做所有权转移。

rust中每个值都有一个所有者,更进一步说就是,rust中分配的每块内存都有其所有者,所有者负责该内存的释放和读写权限,并且每次每个值只能有唯一的所有者,这就是rust的所有权机制(ownership)

所有权的特点

所有者拥有以下三种权限:

  • 控制资源(不仅仅是内存)的释放
  • 出借所有权,包括不可变(共享)的和可变(独占)的
  • 转移所有权

对于可以实现 Copy 的复制语义类型来说,所有权并未改变。对于复合类型来说,是复制还是移动,取决于其成员的类型。

  • 结构体:即使结构体的成员都是复制语义类型,但是rust也不会默认为其实现 Copy。需要手工添加 #[derive(Debug, Copy, Clone)]
  • 枚举:类似结构体
  • 元组:本身实现了 Copy,如果元素均为复制语义类型,则默认是按位复制,否则会执行移动语义。
  • 数组:类似元组
  • Option类型:类似元组

5.3 绑定、作用域和生命周期

Rust 使用 let 关键字来生命变量。let 有 let banding 之意,let 声明的变量实际不是传统意义上的变量,而是指一种绑定语义。let 绑定了标识符和内存,而且使得标识符对那块内存拥有了所有权,因此被称为“绑定”。

5.3.1 不可变与可变

不可变(Immutable)的优点:

  • 多线程并发时,不可变的数据可以安全地在线程间共享
  • 函数的“副作用”可以得到控制

Rust声明的绑定默认不可变,如果需要修改,Rust 提供关键字 mut 来声明可变绑定。

5.3.2 绑定的时间属性-声明周期

变量绑定具有“时空”双重属性:

  • 空间属性:指标识符与内存空间进行了绑定
  • 时间属性:指绑定的时效性,也就是它的生存周期

除了 let 声明外,还有一些场景会创建新的词法作用域:

  • 花括号{}

  • match 匹配

  • 循环语句

  • if let 和 while let 块

  • 函数

    函数参数是复制语义时按位复制,是移动语义时会转移所有权

  • 闭包

    闭包会创建新的作用域,对于环境变量来说有一下三种捕获方式:

    1. 对于复制语义类型,以不可变应用(&T)来捕获
    2. 对于移动语义类型,执行移动语义转移所有权来捕获
    3. 对于可变绑定,如果在闭包中包含对其进行修改的操作,则以可变引用(&mut)来捕获

5.4 所有权借用

引用与借用

引用(Reference)是Rust提供的一种指针语义。应用是基于指针的实现,和指针的区别是:

  • 指针保存的是指向内存的地址
  • 引用可以看做某块内存的别名(alias),使用引用需要满足编译期的各种安全检查规则。

引用分为不可变引用(使用&操作符)和可变引用(使用&mut操作符)。

在所有权系统中,引用&x可称为 x 的借用(Borrowing),通过 & 操作符来完成所有权的租借。借用所有权不会引起变量所有权的转移。

借用所有权会让所有者(owner)受到如下限制:

  • 在不可变借用期间,所有者不能修改资源,并且也不能再进行可变借用
  • 在可变借用期间,所有者不能访问资源,并且也不能再出借所有权

引用在离开作用域之时,就是归还所有权之时。

借用规则

为了保证内存安全,借用必须遵循以下三个规则:

  • 规则一:借用的生命周期不能长于出借方(拥有所有权的对象)的生命周期

  • 规则二:可变借用(引用)不能有别名(Alias,即其他不可变引用),因为可变借用具有独占性

  • 规则三:不可变借用(引用)不能再次出借为可变借用

规则一是为了防止出现悬垂指针,规则二和规则三总结为一条核心原则:共享不可变,可变不共享。规则二和规则三描述的不可变借用和可变借用就相当于内存的读写锁,同一时刻,只能拥有一个写锁,或者多个读锁,不能同时拥有。

Rust的借用检查带来如下好处:

  • 不可变借用保证了没有任何指针可以修改值的内存,便于将值存储在寄存器中
  • 可变借用保证了在写的时候没有任何指针可以读取值的内存,避免了脏读
  • 不可变借用保证了内存不会在读取之后被写入新数据
  • 保证了不可变借用和可变借用不相互依赖,从而可以对读写操作进行自由移动和重新排序

解引用会获得所有权。

5.5 生命周期参数

跨函数使用借用,需要显式地对借用参数或返回值使用生命周期参数进行标注。

5.5.1 显式生命周期参数

生命周期参数必须以单引号开头,参数名通常都是小写字母,比如 'a 。生命周期参数位于引用符号 & 后面,并使用空格来分割生命周期参数和类型。如:

&'a i32;
&'a mut i32;

标注生命周期参数并不能改变任何引用的生命周期长短,它只用于编译器的借用检查,来防止悬垂指针。

函数签名中的生命周期参数

函数签名中的生命周期参数使用如下标注语法:

fn foo<'a>(s: &'a str, t: &'a str) -> &'a str;

函数名后面的 <'a> 为生命周期参数的声明,与范型参数类似,必须先声明才能使用。

  • 输入生命周期(input lifetime):函数或者参数的生命周期
  • 输出生命周期(output lifetime):返回值的生命周期

函数签名的生命周期参数有这样的限制条件:输出(借用方)的生命周期长度必须不长于输入(出借方)的生命周期长度。(遵循借用规则一)

禁止在没有任何输入参数的情况下返回引用。因为明显会造成悬垂指针。

从函数中返回(输出)一个引用,其生命周期参数必须与函数的参数(输入)相匹配,否则,标记生命周期参数毫无意义。

函数生命中的 'a 可以看作是一个生命周期范型参数,输入引用和输出引用都标记为 'a 意味着输出引用(借用方)的生命周期不长于输入引用(出借方)的生命周期。

对于多个输入参数的情况,也可以标注不同的生命周期参数,如:

fn the_longest<'a, 'b: 'a>(s: &'a str, t: &'b str) -> &'a str;

'b: 'a 的意思是范型生命周期参数 'b 的存活时间长于范型生命周期参数 'a (即 'b outlive 'b)。

生命周期参数的目的:生命周期参数是为了帮助借用检查器验证非法借用。函数间传入和返回的借用必须相关联,并且返回的借用生命周期必须比出借方的生命周期短。

生命周期参数是为了帮助借用检查器验证合法的引用,消除悬垂指针。

结构体定义中的生命周期参数

结构体在含有引用类型成员的时候也需要标注生命周期参数。

struct Foo<'a> {
    part: &'a str,
}

这里的生命周期参数标记,实际是和编译器约定了一个规则:结构体实例的生命周期应短于或者等于任意一个成员的生命周期

方法定义中的生命周期参数

需要在 impl 关键字之后申明生命周期参数:

impl<'a> Foo<'a> {
    fun new(s: &'a str) -> Self {
        ......
    }
    ......
}

枚举和结构体对生命周期参数的处理方式是一样的。

静态生命周期参数

字符串字面量是全局静态类型,它的数据和程序代码一起存储于可执行文件的数据段中,其地址在编译器是已知的,无法更改。

在 rust 2018 版本中,使用 const 和 static 定义字符串字面量时,都可以省掉 ‘static 静态生命周期参数。

5.5.2 省略生命周期参数

Rust针对某些场景确定了一些常见的模式,将其编码到Rust编译器中,以便编译器可以自动补齐函数签名中的生命周期参数,这样就可以省略生命周期参数。

被硬编码进编译器的模式被成为生命周期省略规则(lifetime Elision Rule),一共包含三条规则:

  • 每个输入位置上省略的生命周期都将成为一个不同的生命周期参数
  • 如果只有一个输入生命周期的位置(不管是否省略),则该生命周期都将分配给输出生命周期
  • 如果存在多个输入生命周期的位置,但是其中包含找 &self 或者 &mut self,则 self 的生命周期将被分配给输出生命周期。

5.5.3 生命周期限定

生命周期参数可以像 trait 那样作为范型的限定,有以下两种形式:

  1. T: 'a : 表示 T 类型中的任何引用都要"活得" 和 'a 一样长
  2. T: Trait + 'a : 表示 T 类型必须实现 Trait 这个trait,并且T类型中任何引用都要"活得" 和 'a 一样长

5.5.4 trait对象的生命周期

trait 对象和生命周期有默认遵循的规则:

  • trait 对象的生命周期默认是 'static
  • 如果实现 trait 的类型包含 &'a X 或者 &'a mut X,则默认生命周期就是 'a
  • 如果实现 trait 的类型只有 T: 'a,则默认生命周期就是 'a
  • 如果实现 trait 的类型包含多个类似 T: 'a 的从句,则生命周期需要明确指定

5.6 智能指针与所有权

智能指针和普通引用的区别之一就是所有权的不同:

  • 智能指针拥有资源的所有权
  • 普通引用只是对所有权的借用

Box<T> 智能指针可以使用解引用操作符进行解引用。

之所以可以进行解引用,是因为 Box<T> 实现了 deref 方法:

impl<T: ?Sized> Deref for Box<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &**self
    }
}

对于 Box<T> 类型来说,如果包含的类型T属于复制语义,则执行按位复制;如果属于移动语义,则移动所有权。

5.6.1 共享所有权 Rc<T> 和 Weak<T>

Rust 中提供了 Rc<T> 智能指针来支持引用计数。

Rc<T> 可以将多个所有权共享给多个变量。 Rc<T> 主要用于系统共享堆上分配的数据可以供程序的多个部分读取的场景,并且主要确保共享的资源的析构函数都能被调用。

Rc<T> 是单线程引用计数指针,不是线程安全的类型。

  • 强引用: 通过 clone 方法共享的引用所有权被成为强引用
  • 弱引用: 通过 downgrade 方法创建的 Weak<T> 智能指针属于 Rc<T> 的另一种版本,它共享的指针没有所有权,所以被成为弱引用。

5.6.2 内部可变性 Cell<T> 和 RefCell<T>

Rust 中的可变或不可变主要是针对一个变量绑定而言,比如对于结构体来说,可变或者不可变只能对其实例进行设置,而不能设置单个成员的可变性。但是在实际的开发中,某个字段是可变而其他字段不可变的情况是确实存在的。

Rust 提供Cell<T> 和 RefCell<T> 来应对这种情况,他们本事上不属于智能指针,只是提供内部可变性(Interior Mutability)的容器。

Cell<T>

内部可变性实际上是 Rust 中的一种设计模式。

内部可变性容器是对 Struct 的一种封装,表面不可变,但内部可以通过某个方法来改变里面的值。

使用 Cell<T> 内部可变容器确实方便了编程,它提供的 set/get 方法像极了 oop 语言中常见的 getter/setter 方法,封装了对象属性的获取和设置方法。

Cell<T> 通过对外暴露的 get/set 方法实现了对内部值的修改,而其本身却是不可变的。所以,实际上 Cell<T> 包裹的 T 本身合法的避开了借用检查。

对于包裹在 Cell<T> 中的类型 T ,只有实现了 Copy 的类型T,才可以使用 get 方法获取包裹的值,因为 get 方法返回的是对内部值的复制。但是任何类型T都可以使用set方法修改其包裹的值。

对于实现了 Copy 的T,可以任意读取;对于没有实现 Copy 的类型T,则提供了 get_mut 方法来返回可变借用,依然遵循 Rust 的借用检查规则。

Cell<T> 内部每次 get/set 都会执行一次按位复制。

RefCell<T>

对于没有实现 Copy 的类型,使用 Cell<T> 有许多不变。 Rust 提供的 RefCell<T> 适用的范围更广,对类型T并没有 Copy 的限制。

RefCell<T> 提供 borrow/borrow_mut 方法,对应 RefCell<T> 的 get/set 方法。

RefCell<T> 维护一个运行时借用检查器,有运行时开销。

小结

Cell<T> 和 RefCell<T> 使用最多的场景就是配合只读引用来使用,比如 &T 或者 Rc<T> 。

Cell<T> 和 RefCell<T> 之间的区别可以总结为:

  • Cell<T> 使用 set/get 直接操作包裹的值,而 RefCell<T> 通过 borrow/borrow_mut 返回包装过的引用 Ref<T> 和 RefMut<T> 来操作包裹的值。
  • Cell<T> 一般适合复制语义类型(实现了 Copy), RefCell<T> 一般适合移动语义类型(未实现 Copy)
  • Cell<T> 无运行时开销,而且永远不会在运行时引发 panic 错误。 RefCell<T> 需要在运行时执行借用检查,有运行时开销。而且一旦发现违反借用规则的情况,则会引发看线程 panic 。

5.6.3 写时复制Cow<T>

写时复制(Copy On Write / Cow) 是一种优化策略。

Rust 中的 Cow<T> 是一个枚举体的智能指针,包括两个可选值:

  • Borrowed:用于包裹引用
  • Owned:用于包裹所有者

Cow<T> 提供的功能是: 以不可变的方式访问借用内容,以及在需要可变借用或所有权的时候再克隆一份数据Cow<T> 旨在减少复制操作,提供性能,一般用在读多写少的场景。

 

https://skyao.io/learning-rust/docs/ownership/rust-bczd/

标签:生命周期,语言,语义,参数,引用,借用,所有权,rust
From: https://www.cnblogs.com/tryst/p/18374577

相关文章

  • C语言基础--数组详解
    目录数组的概述1数组的概念2数组的分类一维数组1一维数组的定义2数组的访问3数组元素的初始值3.1先定义,后设置值3.2定义同时设置初始值3.2.1全部设置初始值3.2.2部分设置初始值4一维数组的应用实例5一维字符数组5.1一维字符数组的定义5.2一维字符......
  • C语言数据类型和变量(下)
    目录1.算数操作符:+、-、*、/、%1.1+和-1.2*5.3/5.4%2.赋值操作符:=和复合赋值2.1连续赋值6.2复合赋值符3.单目操作符:++、--、+、-3.1++和--3.1.1前置++3.1.2后置++3.1.3前置--3.1.4后置-- 3.2+和- 4.强制类型转换 5.scanf和prin......
  • 微软发布 Phi-3.5 系列模型,涵盖端侧、多模态、MOE;字节 Seed-ASR:自动识别多语言丨 RTE
       开发者朋友们大家好: 这里是「RTE开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享RTE(Real-TimeEngagement)领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「有看点的会议」,但内容仅代表编......
  • 大型语言模型从训练到推理的介绍
    参考论文:https://arxiv.org/pdf/2401.02038v1一、训练方面1、数据预处理(1)除噪音a.去除离群值:使用统计方法(如z-score、IQR)识别并移除异常数据点。importnumpyasnpfromscipyimportstatsdata=np.array([10,12,12,13,12,100])#100是离群值#计算z-s......
  • 03.C语言学习【零基础版】@CHX------算术操作符
    一.+and-+:加法-:减法代码:#include<stdio.h>intmain(){ inta=5+7; intb=20-44; printf("%d%d\n",a,b); return0;}运行结果:二.* *:乘法代码:#include<stdio.h>intmain(){ inta=5+7; intb=20-18; printf("%d\n"......
  • 【C语言进阶】数据如何安家?C语言内存中的存储艺术深度解析
    ......
  • 软考-软件设计师(程序设计语言习题)
       ......
  • Go语言基础--函数基本介绍及包
    Go语言(也称为Golang)的函数是执行特定任务的代码块。它们允许你重用代码,让程序更加模块化和易于维护。在Go中,函数可以接收参数(输入值)并返回结果(输出值)。下面详细解释Go语言中的函数:函数定义函数通过 func 关键字定义。基本结构如下:funcfunctionName(parameterList)......
  • Android开发语言Kotlin简介
    官方认可:自2017年Google正式宣布Kotlin成为Android开发的官方语言后,它在Android开发中的流行度就有了显著提升。与Java的兼容性:Kotlin在设计时就考虑到了与Java的互操作性,这让开发者能够在Android项目中轻松使用Kotlin,同时继续利用现有的Java代码和库。......
  • LLM | 一文带你揭秘大语言模型ChatGPT的原理
    本文包含大量AI干货预计阅读时间:10分钟本文学习目标:定义语言模型和大型语言模型(LLM)。介绍关键的LLM概念,包括TransFormer和自注意力机制。介绍LLM提示工程、微调和Rag,以及当今热门的大语言模型应用。前言在当今的科技时代,大型语言模型(LLM)正以惊人的速度发展并......