缘起

本来这一系列文章并不在计划中。昨天跟赵磊和七哥讨论没有GC管理内存的问题。
讨论到没有GC情况下管理内存的学习曲线,七哥认为学习曲线不陡而是使用曲线陡。诚然,如果只有malloc和free,确实还是学习容易使用难的。到了C++引用了new和delete之后,学习曲线也还算是平和的,因为后面还有auto_ptr, 自动引用计数,右值引用和std::move等一大堆要学习的慢慢地增加进来。多线程的情况下,还会有更复杂一些的问题。
但是这比起Rust语言来,学习曲线都要好一些,因为大不了是memory leak,起码还可以编译通过。而在Rust语言中,初始的这个小陡坡如果踏不过去的话,可能会连编译都编不过。

这就是Rust的设计原则,不希望有一个比较重的运行时,通过编译时的勤奋来减少运行时的麻烦。

Rust语言简介

Rust是Mozilla推出的,希望能够在多核时代充分发挥硬件能力的语言。Rust能够引起大家的关注,最初的原因是它的设计者之一的Brendan Eich是著名的JavaScript语言之父。不管程序员对于JavaScript有多少的诟病,JavaScript的成功是个难以重制的传奇。JavaScript在OOP如日中天的年代将self的原型链和scheme的函数式编程思想结合起来,显示出了Brendan Eich的功力。但是,JavaScript的最初版本的开发时间太短,没有充分给Brendan Eich发挥自己能力的机会。后来,有了历史包袱之后,就不是想怎么改就怎么改的情况了。JavaScript成为ECMA Script之后,4.0版本的被废弃掉,就是这个挫折的表现。
Rust是门新语言,三位主要创造人可以放开手脚去发挥。他们也确实是这么做的,语法不停改来改去。比如Rust最初是支持Go一样的轻量级多任务支持的,后来就被砍掉了,现在就是传统的线程之间像Go一样在来回发消息。
不过,其实Rust最早的创始人是Graydon Hoare. 他在2006年就开始开发了Rust,最早的编译器用OCaml来实现的。2009年,Hoare在Mozilla公司的支持下继续开发Rust。
2015年5月16日,Rust正式发布1.0版本。Hoare虽然离开了Mozilla,但是Rust在Mozilla的支持下在继续发展。

用Rust语言开发的Servo是一个类似Webkit的浏览器引擎。

Rust语言的一个重要特点就是没有一个厚重的运行时,所以也就没有GC这样的运行时的设施。坏处是我们得花精力学习在无GC情况下的设计,就是我们这一讲想做到的事情。好处是,轻量,可以用于系统编程,代码可以跑在裸机上,而不是一套运行时环境上。这样,理论上,Rust编译的代码可以像C和C++语言一样轻,可以用于嵌入式系统级软件开发,比如IoT设备上。做为对比的是Go语言,它有一个运行时,写应用的话Go很可能会胜出,但是在写系统级软件时,这个运行时环境可能就不是最理想的基础设施了。
举个小例子,比如Go和Rust同样来为Java写JNI代码。Rust就可以被当成C/C++用就是了,但是Go就会引入一个很好玩的问题,Java有GC,Go也有GC,这样的JNI代码如何处理二者间引用对象的问题?
Rust和C++之间基本可以无缝调用,反正本质上的思想也差不多。
Rust支持函数式的思想,支持C++所没有的类似于函数语言的宏。Rust支持泛型,支持traits,这是C++程序员所喜欢,而Java这样只是个语法糖方式所无法想象的。

常与Rust和Go一起比较的,还有Apple的swift语言和Digital Mars的D语言。它们的共同特点是高生产力的语法和全编译带来的高效率。

好啦,我们也先来一个Hello,World程序跟Rust语言打个招呼吧.
首先去官网下载Rust的包:https://www.rust-lang.org
在mac上,就用brew install rust就好了。
IDE的话,我用的是Visual Studio Code,大家可以自由选择。

fn main(){let s = "Hello,Rust!";println!("{}",s);
}

调用rustc,或者是通过cargo,就可以编译运行啦。
可以通过cargo init去创建一个工程,之后用cargo build或者cargo run就好了。
println!是一个输出用的宏,不要忘了后面那个提示它是个宏的叹号哟。

Rust是一门没有GC的语言

好吧,闲话少说,我们开始面对学习一门类似于C++的,没有GC的新型语言的第一道关口,管理内存吧。

Rust同C++一样,存储空间既有堆,也有栈。对象放在栈上,可以复制,也可以随着退栈而被自动销毁掉。在C引入malloc之前,要么是静态分配,要么是在栈上分配,并没有复杂的堆上内存泄漏的难题。而以Java为代表的一类语言,默认都是在堆上分配对象,所以需要GC来实现堆上的管理。

我们也以容器为例来说明这个问题:

    let v = vec![1,2,3];println!("{}",v[0]);

vec!宏用于初始化一个vector容器。let叫做绑定,将值绑定在一个变量上。
v变量本身分配在栈里面,而v中的三个元素是分配在堆里面。当函数退出之后,v的作用域结束了,它所引用的堆中的元素也会被自动回收。听起来很不错,至少比malloc和free一定要配对强。

下面问题来了,如果想要将v的值绑定在另一个变量v2上,会出现什么情况呢?
对于有GC的系统来说,这不是问题,v和v2都引用同一个堆中的引用,最终由GC来回收就是了。
而对于没有GC的系统,有三种选择:
1. 复制:将堆中的数据重制一份,绑定到新的变量上。C++中的栈上的对象的赋值就是这么做的,会调用一个叫做拷贝构造函数的东西来负责这个复制。
2. 移动:C++11引入的std::move语义就是做这个的,将所有权转移。堆上的值被v2所引用,v的引用失效。
3. 引用计数:采用自动引用计数的方式,当引用计数为0的时候就释放掉堆上的数据。C+、+11和Boost的shared_ptr就是这种思想哈。

Rust默认是移动语义

C++是默认以复制语义为主的,而Rust默认是移动语义。
在Rust中,如果引用了移动了控制权之后的对象中的元素,会是什么下场呢?答案是,根本编不过。
假如说我们这么写:

    let v = vec![1,2,3];let v2 = v;println!("{}",v[0]);

编译器就给我们报这样的错误,直接编译失败。
其实编译器给出的错误信息还是蛮友好的哈。

error[E0382]: use of moved value: `v`--> src/main.rs:7:16|
6 |     let v2 = v;|         -- value moved here
7 |     println!("{}",v[0]);|                   ^ value used here after move
<std    macros>:2:27: 2:58 note: in this expansion of format_args!
<std    macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
src/main.rs:7:2: 7:22 note: in this expansion of println! (defined in <std macros>)|= note: move occurs because `v` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait

编译失败告诉我们,除非实现了Copy trait,否则一个类型的默认是移动语义的。

好吧,你认了,默认移动就移动呗。小心啦,我们看看下面这个例子。
我们只是调个无害的函数嘛:

fn take(v: Vec<i32>){println!("I did nothing on v!");
}fn main(){let v = vec![1,2,3];take(v);println!("{}",v[0]);
}

编译结果是什么?

error[E0382]: use of moved value: `v`--> src/main.rs:8:16|
7 |     take(v);|          - value moved here
8 |     println!("{}",v[0]);|                   ^ value used here after move
<std    macros>:2:27: 2:58 note: in this expansion of format_args!
<std    macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
src/main.rs:8:2: 8:22 note: in this expansion of println! (defined in <std macros>)|= note: move occurs because `v` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait

竟然跟上面的绑定到另一个变量上一样,v的所有权已经被别的变量给拿走了。

这可如何是好呢?
其实也没什么神秘的。正如带有GC的语言一般都会引用弱引用一样,这时需要的是一个不引起所有权转移的方式,就是Rust语言的引用。采用与C++类似的符号,在类型前加上&就表示引用啦。
改写成下面的引用的方式,就可以正常编译了:

fn take(v: &Vec<i32>){println!("I did nothing on v!");
}fn main(){let v = vec![1,2,3];take(&v);println!("{}",v[0]);
}

在Rust里面,引用的方式有个新的名字叫做“borrow”-借用。只管用,不管生命周期的销毁。跟弱引用很类似,大家应该能很好的理解。

Rust是不变性优先的语言

Rust语言在设计上,就是为线程安全多做考虑的一门语言。而什么样的数据在多线程下最安全呢?答案是不变的数据。Java世界的并发名著《Java Concurrency in Practice》在讲如何使用丰富的多任务工具之前,先讲了如何做状态封闭。
Rust的设计上也充分认识到了这一点。为什么其他语言是叫变量赋值,而在Rust就强调是变量绑定呢?其重要原因就是这个绑定默认是不变的。
比如我们默认定义个变量绑定,再对这个变量进行操作,会导致,编译不过。
我们试个最简单的例子:

    let a = 1;a = a + 2;

编译报下面的错:

  --> src\main.rs:10:5|
9  |     let a = 1;|         - first assignment to `a`
10 |     a = a + 2;|     ^^^^^^^^^ re-assignment of immutable variable

那么如果要定义可以改变的绑定可不可以呢?当然可以,但是在Rust里,这不是默认的行为,需要加mut关键字来说明,我们将上面的例子改写一下,在let后面加上mut:

    let mut a = 1;a = a + 2;println!("{}",a);

这就是Rust不同于很多其他语言的地方,比如C++和Java,它们是默认是可变的,不可变的要加const和final。而Rust反过来了,不变的是正常的,可变的要加mut。

比如我想要给vector v增加一个值:

    let v = vec![1,2,3];v.push(4);

会导致编译失败:

 --> src\main.rs:7:5|
6 |     let v = vec![1,2,3];|         - use `mut v` here to make mutable
7 |        v.push(4);|        ^ cannot borrow mutably

也是需要改成mut的才可以push:

    let mut v = vec![1,2,3];v.push(4);

有了默认是不变性的知识,我们再回头来看导致所有权转移的这条语句:

    let v = vec![1,2,3];let v2 = v;

实在是有点不人道啊,因为v和v2都不是mut的,都是常量啊。

为什么两个常量还是不可以共享同一份堆数据呢?答案很简单,因为两个不变量的生命周期可能是不一致的,默认的变量绑定可是没有引用计数这样的功能的,它是一直跟一个变量的生命周期所绑定的。

同样,引用默认也是不变的,要想改变引用的值,也需要加mut。
比如下面的还是编译不过的:

fn take(v: &Vec<i32>){v.push(4);
}

报同样的错:

error: cannot borrow immutable borrowed content `*v` as mutable--> src\main.rs:3:5|
3 |     v.push(4);|     ^

我们同样要用mut来修饰它,将凡是&出现的地方都换成&mut

fn take(v:  &mut Vec<i32>){v.push(4);
}fn main(){let mut v = vec![1,2,3];take(&mut v);println!("{}",v[3]);
}

输出结果是4,终于大功告成了!

多重借用

下面我们继续思考一个问题,刚才都是只有一个借用者,只有一个引用,那么有多个引用会如何?
比如这样是不是可以?

    let mut v = vec![1,2,3];take(&mut v);println!("{}",v[3]);let v2 = &v;let v3 = &v;

是的,上面这些是可以的。

那么既有不变的,又有可变的,可不可以呢?

    let v2 = &v;let v3 = &v;let mut v4 = &mut v; 

结果是,又报错编不过了:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable--> src\main.rs:12:23|
10 |     let v2 = &v;|               - immutable borrow occurs here
11 |     let v3 = &v;
12 |     let mut v4 = &mut v;|                       ^ mutable borrow occurs here
13 | }| - immutable borrow ends here

这又是Rust对于线程安全的考虑,如果大家都是只读的引用,可以有任意多个。但是只要有一个是可变的,对不起,那就只有这一个可以引用。违反了这条规则,不是运行时出问题,而是编译都不能通过。

那么一段代码中,业务逻辑就是要多于一种可变的引用来操作它怎么办?
解决方案是,分时操作。只要不是同时针对一个变量作引用就好了,我们可以通过作用域的方式将它们进行分时,比如这样做:

    {let v2 = &v;let v3 = &v;}{let mut v4 = &mut v;}

只加了两对括号,顺序上没有任何明影响。但是因为错开了作用域的重叠,就可以顺利编译通过,而且也避免了风险。

但是,要注意,一个长的作用域的引用,想要引用一个生命周期比它短的对象是编译不过的。
比如上面我们是在大作用域内定义对象,子作用域里定义引用,我们反过来,在大定义域里定义引用,引用子作用域里的对象,像这样:

    let vv: &mut Vec<i32>;{let vv2 = vec![4,5,6];vv = &mut vv2;}

看起来不错,但是编不过:

  --> src\main.rs:21:19|
21 |         vv = &mut vv2;|                   ^^^|
note: reference must be valid for the block suffix following statement 5 at 18:26...--> src\main.rs:18:27|
18 |     let vv: &mut Vec<i32>;|                           ^
note: ...but borrowed value is only valid for the block suffix following statement 0 at 20:30--> src\main.rs:20:31|
20 |         let vv2 = vec![4,5,6];|                               ^

小结一下

  • Rust没有GC
  • Rust的对象,默认是移动语义。在特殊情况下也支持复制语义。但是不管是移动还是复制,变量之间不能共享同一份堆上的数据
  • 要想共享对象的数据,需要通过引用来借用。同一作用域内同时只能有一个可变的引用。没有可变的引用时,支持多个不变引用共享。
  • Rust的变量绑定默认是不变的,可变的需要加mut关键字说明。引用也是如此。
  • 较大作用域的引用不能引用活得比它短的小作用域内的对象,反之可以。

Rust语言教程(1) - 一门没有GC的语言相关推荐

  1. Rust —— 一门没有GC的语言

    Rust语言教程(1) - 一门没有GC的语言 来自:Jtag特工 原文:https://www.jianshu.com/p/67c94459997a 缘起 本来这一系列文章并不在计划中.昨天跟一些同 ...

  2. 红黑联盟c语言教程,1.1.2 C语言的特点

    C语言与C#.C++和Java有着密切的"血缘"关系,可以说C语言是这几种语言的"鼻祖",因为这几种语言都是在C语言的基础上发展起来的,所以常常能听到" ...

  3. c语言教程github,GitHub - saitjr/C-Toturials: C语言教程

    #写在开头 ##关于写作目的 C语言是我接触的第一门程序语言,或多或少还是有那么一点"初恋情结".在之后的开发中,不管是做Java,还是做PHP,还是现在在做的iOS,我都不愿意丢 ...

  4. c语言教程指针,(转)C语言指针5分钟教程

    指针.引用和取值 什么是指针?什么是内存地址?什么叫做指针的取值?指针是一个存储计算机内存地址的变量.在这份教程里"引用"表示计算机内存地址.从指针指向的内存读取数据称作指针的取值 ...

  5. 机器人c语言教程pdf,机器人卡雷尔学习C语言.pdf

    机器人卡雷尔学习 C 语言 新疆农业大学计算机与信息工程学院 张太红.陈燕红编著,2013 年 9 月 斯坦福大学计算机科学系 埃里克·罗伯茨著 (Java版),2005年9月 目 录 第一章 机器人 ...

  6. 自创计算机语言,【图片】【自创语言教程】———创造属于自己的语言!(上)【那些漫长岁月吧】_百度贴吧...

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 - [人造语言的历史] -Constructed Language,简称Conlang,而玩Conlang的人则自称为Conlanger.中文将之译成人造 ...

  7. 新概念c语言教程答案,新概念编程C语言篇习题解答

    摘要: <21世纪普通高校计算机公共课程规划教材:新概念编程C语言篇习题解答>对<新概念编程C语言篇>教材中的习题进行了系统全面的分析和解答.习题精选了C语言编程中典型题型,计 ...

  8. c语言教程项目一实验报告,C语言实验报告(四)

    实验项目: 5.3.1练习2 求数列的前n项的和 5.3.2练习2 求水仙花数 5.3.4 十进制转换 5.3.5练习1 白马百担问题 5.3.5练习2 等差数列 5.3.5练习3 食品分配问题 九九 ...

  9. Swift3.0语言教程使用URL字符串

    Swift3.0语言教程使用URL字符串 Swift3.0语言教程使用URL字符串,和路径一样,URL其实也是字符串,我们可以将这些字符串称为URL字符串.本小节将讲解URL字符串的使用. 1.编码 ...

最新文章

  1. IAR for msp430 MDK中 warning: #223-D: function xxx declared implicitly 解决方法
  2. 丢掉uVision2吧,uVision早就有了
  3. linux 符号链接攻击防御简介
  4. 更改Visual Studio 2010/2012/2008的主题设置
  5. 中国碳酸氢钠干粉灭火剂市场产销分析与盈利前景策略报告2022年
  6. MongoDB 小试牛刀
  7. CaSS中lisp命令不可用_小白想要自学南方cass?又苦于不会修改比例尺,这里有一份小白快速上手的测绘神器...
  8. html 让表格在右侧显示不出来,css中怎么解决表格边框不显示的问题?
  9. Qt-线程启动与关闭实例
  10. 【NOIP考前模拟赛】纯数学方法推导——旅行者问题
  11. ②你真的学会Java了吗?来自《卷Ⅰ》的灵魂提问
  12. directui 3d界面引擎_美术设计师浅谈AR/VR中3D建模设计的工具、挑战与区别
  13. 唯美“光效”PNG免扣素材大集合,一眼爱上!
  14. 【imessage软件群推送】 “CMCC“ | grep password #待补充 重置后撤销暂存的变更
  15. ads pspice 导入_ADS中使用pspice模型
  16. 定义一个函数,返回整形数组中最大值
  17. CentOS添加新硬盘和硬盘格式化
  18. postman中文汉化版
  19. 【verilog】【Modelsim仿真】“XXX“already declared in this scope
  20. java 复制excel_Java 复制Excel工作表

热门文章

  1. 使用花生壳配置内网穿透
  2. 使用Trinamic TMC2300步进驱动器做一个迪斯科灯项目
  3. 单片机直流电机驱动电路
  4. 农场工具程序设计(三)
  5. 基于ESP32与OLED12864的主机状态监视器(MicroPython)
  6. LLJ-100HS; LLJ-500HS漏电继电器
  7. python桌面爬虫_爬虫项目实战十三:爬取zol桌面壁纸
  8. enti下载器_短跑enti策略:如何在不破坏软件的情况下改进软件
  9. linux的qt下wsadata,Qtcpserver仅在调试器下返回listen函数的未知错误
  10. IReport 使用记录 主报表和子报表数据传递