编程语言的内存管理,大概可以分为自动和手动两种。

  自动管理就是用 GC(垃圾回收)来自动管理内存,像 Java、Ruby、Golang、Elixir 等语言都依赖于 GC。而 C/C++ 却是依赖于手工管理内存,程序员使用 malloc 和 free 函数来分配释放内存。

  GC技术经过这么多年的发展,是相对安全的内存管理,也解放了程序员,但是在一些系统级编程领域,实际上是需要避免 GC,因为 GC 会引起“世界暂停”,这将带来性能问题,所以在系统级编程领域C/C++占绝对的霸主地位。

  但是,有C/C++就够了吗?靠手工来管理内存,会带来很多安全问题,比如悬垂指针,诚然有最佳实践引导,就算经验丰富的熟手也难以避免类似内存安全的错误。

  Rust 的出现,就是为了解决这个痛点,它强大的所有权系统,就像是黑暗中的明灯。

  我曾经也对其感到疑虑,这凭空产生的 Rust 的所有权系统是不是拍脑袋发明的,这真的是解决内存安全问题的“银弹”吗?

  其实历史上也曾经有过解决内存安全问题的努力,比如 Cyclone 语言,它是一门对 C 语言进行安全升级的语言,基于区域(region,有点和 Rust 所有权系统中的生命周期相类似)的内存管理,避免一些潜在的内存安全问题,但是,功能极其有限,类似的尝试还有ML Kit。

  就是这些早期的方案,给了Rust语言灵感,才造就现在的所有权系统,所以Rust的所有权系统并非凭空产生。至于是不是“银弹”,还不敢下结论,至少,Rust的所有权系统是迄今为止最精妙最科学的方案了。

  语义模型

  什么叫语义模型?语义,顾名思义,是指语言的含义。我们在学习一个新概念的时候,首先就要搞明白它的语义。而语义模型,是指语义构建的心智模型,因为概念点不是孤立存在的,彼此之间必然有紧密的联系,我们通过挖掘其语义之间的关联规则,在你的认知中形成一颗“语义树”,这样的理解才是通透的。所有权的“语义树”,如下图所示:

  上图中的语义树,主要是想表达下面几层意思:

  ● 所有权是有好多个概念系统性组成的一个整体概念。

  ● let绑定,绑定了什么?变量 + 作用域 + 数据(内存)。

  ● move、lifetime、RAII都是和作用域相关的,所以想理解它们就先要理解作用域。

  所有权

  所有权,顾名思义,至少应该包含两个对象:“所有者”和“所有物”。在Rust中,“所有者”就是变量,“所有物”是数据,抽象来说,就是指某一片内存。let关键字,允许你绑定“所有者”和“所有物”,比如下面代码:

  let num = String::from("42");

  let 关键字,让 num 绑定了42,那么可以说,num拥有42的所有权。但这个所有权,是有范围限制的,这个范围就是作用域(scope),准确来说是拥有域(owner scope)。换句话说,num在当前作用域下,拥有42的所有权。如果它要进入别的作用域,就必须交出所有权。比如下面的代码:

  let num = String::from("42");

  let num2 = num;

  let关键字会开启一个隐藏作用域,我们可以借助于MIR来查看,编译这两行代码,查看其MIR:

  scope 1 {

  let _1: std::string::String; // "num" in scope 1 at :4:8: 4:11

  scope 2 {

  let _2: std::string::String; // "num2" in scope 2 at :5:8: 5:12

  }

  }

  Scope 1就是num所在的作用域,scope 2是num2所在的作用域。当你此时想像下面这样使用num的时候:

  let num = String::from("42");let num2 = num;println!("{:?}", num);

  编译器会报错:error[E0382]: use of moved value: `num`。因为num变量的所有权已经转移了。

  移动(move)语义

  移动,是指所有权的转移。什么时候所有权会转移呢?就是当变量切换作用域的时候,所谓移动,当然是从一个地方挪到另一个地方。其实你也可以这样认为,当变量切换到另一个作用域,它在当前作用域的绑定将会失效,它拥有的数据则会在另一个作用域被重新绑定。

  但是对于实现了Copy Trait的类型来说,当移动发生的时候,它们可以Copy的副本代替自己去移动,而自身还保留着所有权。比如,Rust中的基本数字类型都默认实现了Copy Trait,比如下面示例:

  let num = 42;

  let num2 = num;

  println!("{:?}", num);

  此时,我们打印num,编译器不会报错。num已经move了,但是因为数字类型是默认实现Copy Trait,所以它move的是自身的副本,其所有权还在,并未发生转移,通过编译。不过需要注意的是,Rust 不允许自身或其任何部分实现了Drop trait 的类型使用Copy trait。

  当move发生的时候,所有权被转移的变量,将会被释放。

  作用域(Scope)

  没有GC帮助我们自动管理内存,我们只能依赖所有权这套规则来手工管理内存,这就增加了我们的心智负担。而所有权的这套规则,是依赖于作用域的,所以我们需要对Rust中的作用域有一定了解。

  我们在之前的描述中已经见过了隐式作用域,也就是在当前作用域中由let开启的作用域。在Rust中,也有一些特殊的宏,比如println!(),也会产生一个默认的scope,并且会隐式借用变量。除此之外,更明显的作用域 范围则是函数,也就是说,一个函数本身,就是一个显式的作用域。你也可以使用一对花括号({})来创建显式的作用域。

  除此之外,一个函数本身就显式的开辟了一个独立的作用域。比如:

  fn sum(a: u32, b: u32) -> u32 {

  a + b

  }

  fn main(){

  let a = 1;

  let b = 2;

  sum(a, b);

  }

  上面的代码中,当调用sum函数的时候,a和b当作参数传递过去,此时就会发生所有权move的行为,但是因为a和b都是基本数据类型,实现了Copy Trait,所以它们的所有权没有被转移。如果换了是没有实现Copy Trait的变量,所有权就会被转移。

  作用域在Rust中的作用就是制造一个边界,这个边界是所有权的边界。变量走出其所在作用域,所有权会move。如果不想让所有权move,则可以使用“引用”来“出借”变量,而此时作用域的作用就是保证被“借用”的变量准确归还。

  引用和借用

  有的时候,我们并不想让变量的所有权转移,比如,我写一个函数,该函数只是给某个数组插入一个固定的值:

  fn push(vec: &mut Vec) {

  vec.push(1);

  }

  fn main(){

  let mut vec = vec![0, 1, 3, 5];

  push(&mut vec);

  println!("{:?}", vec);

  }

  此时,我们把数组vec传给push函数,就不希望把所有权转移,所以,只需要传入一个可变引用&mut vec,因为我们需要修改vec,这样push函数就得了vec变量的可变借用,让我们去修改。push函数修改完,会将借用的所有权归还给vec,然后println!函数就可以顺利使用vec来输出打印。

  引用非常方便我们使用,但是如果滥用的话,会引起安全问题,比如悬垂指针。看下面示例:

  let r;

  {

  let a = 1;

  r = &a;

  }

  println!("{}", r);

  上面代码中,当a离开作用域的时候会被释放,但此时r还持有一个a的借用,编译器中的借用检查器就会告诉你:`a` does not live long enough。翻译过来就是:`a`活的不够久。这代表着a的生命周期太短,而无法借用给r,否则&a就指向了一个曾经存在但现在已不再存在的对象,这就是悬垂指针,也有人将其称为野指针。

  生命周期

  上面的示例中,是在同一个函数作用域下,编译器可以识别出生命周期的问题,但是当我们在函数之间传递引用的时候,编译器就很难自动识别出这些问题了,所以Rust要求我们为这些引用显式的指定生命周期标记,如果你不指定生命周期标记,那么编译器将会“鞭策”你。

  struct Foo {

  x: &i32,

  }

  fn main() {

  let y = &5;

  let f = Foo { x: y };

  println!("{}", f.x);

  }无锡妇科医院哪家好 http://www.wxbhnkyy39.com/

  上面这段代码,编译器会提示你:missing lifetime specifier。这是因为,y这个借用被传递到了 let f = Foo { x: y }所在作用域中。所以需要确保借用y活得比Foo结构体实例长才行,否则,如果借用y被提前释放,Foo结构体实例就会造成悬垂指针了。所以我们需要为其增加生命周期标记:

  struct Foo<'a> {

  x: &'a i32,

  }

  fn main() {

  let y = &5;

  let f = Foo { x: y };

  println!("{}", f.x);

  }

  加上生命周期标记以后,编译器中的借用检查器就会帮助我们自动比对参数变量的作用域长度,从而确保内存安全。

  再来看一个例子:

  fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

  if x.len() > y.len() {

  x

  } else {

  y

  }

  }

  fn main() {

  let a = "hello";

  let result;

  {

  let b = String::from("world");

  result = longest(a, b.as_str());

  }

  println!("The longest string is {}", result);

  }

  此段代码,编译器会报错:`b` does not live long enough。这是因为result在外部作用域定义的,result的生命周期是和main函数一样长的,也就是说,在main函数作用域结束之前,result都必须存活。而此时,变量b在花括号定义的作用域中,出了作用域b就会被释放。而根据longest函数签名中的生命周期标注,参数b的生命周期必须和返回值的生命周期一致,所以,借用检查器果断的判断出`b` does not live long enough。

  “显式的指定”,这是Rust的设计哲学之一。这对于新手,尤其是习惯了动态语言的人来说,可能是一个心智负担。显式的指定方便了编译器,但是对于程序员来说略显繁琐。不过为了安全考虑,我们就欣然接受这套规则吧。

  首发于知乎专栏

Rust所有权语义模型的更多相关文章

  1. 第二篇:gradle脚本运行环境分析(gradle的语义模型)

    引言:通过上一篇的论述,我们知道gradle脚本是如假包换的groovy代码,但是这个groovy代码是运行在他的上下文环境里面的,学名叫语义模型.这一篇我们就来看看他的语义模型到底是什么,如何使用. ...

  2. Rust所有权及引用

    Rust 所有权和借用 Rust之所以可以成为万众瞩目的语言, 就是因为其内存安全性. 在以往内存安全几乎全都是通过GC的方式实现, 但是GC会引来性能.CPU以及Stop The World等问题, ...

  3. 对Rust所有权、借用及生命周期的理解

    Rust的内存管理中涉及所有权.借用与生命周期这三个概念,下面是个人的一点粗浅理解. 一.从内存安全的角度理解Rust中的所有权.借用.生命周期 要理解这三个概念,你首要想的是这么做的出发点是什么-- ...

  4. rust所有权

    所有权与函数 fn main() { let s = String::from("hello"); takes_ownership(s); //s的值移动到函数里 let x = ...

  5. Rust <4>:所有权、借用、切片

    tips:栈内存分配大小固定,访问时不需要额外的寻址动作,故其速度快于堆内存分配与访问. rust 所有权规则: 每一个值在任意时刻都有且只有唯一一个所有者 当所有者离开作用域时,这个值将被丢弃 所有 ...

  6. 最强肉坦:RUST多线程

    Rust最近非常火,作为coder要早学早享受.本篇作为该博客第一篇学习Rust语言的文章,将通过一个在其他语言都比较常见的例子作为线索,引出Rust的一些重要理念或者说特性.这些特性都是令人心驰神往 ...

  7. 百度Android语音识别SDK语义理解与解析方法

    百度语义理解开放平台面向互联网开发人员提供自然语言文本的解析服务,也就是能够依据文本的意图解析成对应的表示. 为了易于人阅读,同一时候也方便机器解析和生成,意图表示协议採用 json 语言进行描写叙述 ...

  8. 浅谈隐语义模型和非负矩阵分解NMF

    本文从基础介绍隐语义模型和NMF. 隐语义模型 ”隐语义模型“常常在推荐系统和文本分类中遇到,最初来源于IR领域的LSA(Latent Semantic Analysis),举两个case加快理解. ...

  9. Roslyn入门(二)-C#语义

    先决条件 Visual Studio 2017 .NET Compiler Platform SDK Rosyln入门(一)-C#语法分析 简介 今天,Visual Basic和C#编译器是黑盒子:输 ...

随机推荐

  1. postman接口测试系列: 时间戳和加密

    在使用postman进行接口测试的时候,对于有些接口字段需要时间戳加密,这个时候我们就遇到2个问题,其一是接口中的时间戳如何得到?其二就是对于现在常用的md5加密操作如何在postman中使用代码实现 ...

  2. Luogu P3946 ことりのおやつ(小鸟的点心) 【最短路】By cellur925

    题目传送门 日本的冬天经常下雪.不幸的是,今天也是这样,每秒钟雪的厚度会增加q毫米. 秋叶原共有n个地点,编号从1到n.每个地点在开始的时候的积雪高度为hi. 有m条双向道路连接这些地点,它们的长度分 ...

  3. the little schemer 笔记(2)

    第二章 Do it, Do it Again, and Again, and Again... 假设l是 (Jack Sprat could eat no chicken fat) 那么 (lat? ...

  4. Tcp实现省略编码

    import socket class My_socket(socket.socket): def __init__(self, encoding='utf-8'): self.encoding = ...

  5. (转)深入理解Java对象的创建过程

    参考来源:http://blog.csdn.net/justloveyou_/article/details/72466416 摘要: 在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一 ...

  6. 创建表规范 lob 字段

    ORAClce 11g 提供如下特性: BasicfileOracle10g 及之前版本被称为basicfile Securefile11g中新增securefile 优点:集中写入缓存(WGC),4 ...

  7. KEIL软件仿真死在等待外部晶振起振

    这是由于是Debug里面的设置有问题 主要是下面2项设置 Dialog DLL默认是DCM3.DLL Parameter默认是-pCM3 应改为 Dialog DLL默认是DARMSTM.DLL Pa ...

  8. Excel数据直接到DataTable--->DB

    1) Excel数据直接导入到临时生成的DataTable using (OleDbConnection selectConnection = new OleDbConnection("Pr ...

  9. Winform datagridview 基础

    ======================================================================================== == 重点需要掌握 A ...

  10. slimScroll的应用(一)

    本类文章依旧是针对初学者来说的,希望大家看到后觉得有用的能给个赞~~ 什么是slimScroll? 一.官网介绍: slimScroll is a small (4.6KB) jQuery plugi ...