“
关于 Go 的讨论和学习,我们放在后期再来进行。后期我们将围绕垃圾回收、并发调度和 CSP 通信模型以及网络编程这些核心原理展开讨论,在这其中同时也会涉及到 context 和 mutex 这些高级特性。这些内容将耗费大量精力去成体系的讨论,在这其中我将翻阅大量资料以及源码,力争给大家展现出这些知识的精华所在。这是一项周期较长的作品,希望我们一起保持初心并且耐心等待。
”
01
Rust & Go
程序员是不会只写一门语言的,今天和大家分享的是我最近很感兴趣并投入大量精力去学习的天然保证内存安全的语言 Rust。作为刚刚接触 Rust 的新人来说,迫不及待地想要拿它与我之前最熟悉的语言 Go 来作一番比较。那么 Go 是否和 Rust 相像呢?下面从相似之处和不同之处来展开讨论。
内存安全
说到内存安全,在今天看来 C++ 已经有了它过时的地方。在安全性上来说有很多的不足之处,比如说不安全或者不正确的访问内存。而在 Rust 和 Go 中虽然两者是以不同的方式处理此问题,但是目的都是具备更加安全的内存管理。Go 中虽然保证了内存安全,但仍然保留了空指针。Rust 中则在设计上就天然避免了空指针出现的可能性,关于这一点以及处理方式我们将在后文中详谈。
性能
Rust 和 Go 都是可以直接被编译为本机代码,而无需通过解释器或者虚拟机来完成。但是 Rust 的性能是更加出色的,与 C/C++ 不同的是它还提供了内存安全性和并发安全性,但是从执行上看却仍相当于 C/C++ 的执行速度。尽管 Go 的性能也非常好,但两者的关注重点是不同的,Go 主要是提高开发速度而不是执行速度,Go 更关心如何快速编译大量代码,而不是像 Rust 关心生成高效的机器代码来加快执行效率。
Rust 中是没有垃圾回收的概念的,所以它的运行性能自始至终是一样的并且可以预测。虽然 Go 的垃圾回收器非常高效,并且在新版本中经过混合写屏障和强三色弱三色的优化后 STW 的时间已经尽可能短,但是只要终止,就仍然会存在不可预测性。所以综合来说在性能上 Rust 是明显要优于 Go。
03
为什么选择 Rust ?
Rust 是一种预编译的静态类型语言。一方面是静态安全的,保证了内存安全和避免了野指针的出现。一方面通过编译期的边界检查索引和断言,以及任务失败不可恢复的机制保证了动态安全。Rust 主要用于系统编程,比如说写操作系统、浏览器内核和开发相关的中间件等。Rust 中最主要的是其内存安全的特性,无需 GC 或者手动释放内存,在编译阶段就可以发现存在的问题,所以执行效率很高。
我们将分为三个部分来展开对 Rust 的讨论。前期通过两篇文章来熟悉基础知识,对 Rust 有一个基本的概念和认识。中期几篇文章来浅谈 Rust 的高级特性以及核心价值,最后我们通过实现一个裁剪版的 serde 序列化框架来完成对 Rust 第一阶段的学习。
03
进击的 Rust
2.0 Hello World
Rust 创建文件总是以.rs 扩展名来结尾,如果文件名中包含多个单词,一般使用下划线来分割它们。下面让我们来创建第一个文件 hello_world.rs
fn main() { println!("Hello, world!");}
println!调用了一个 Rust 宏,当看到符号!的时候就意味着调用的是宏而不是一般函数。关于宏的概念留在中期讨论,现在只需要知道“Hello World”字符串作为一个参数传递了宏,所以这个字符串输出在了命令行中。
2.1 数据类型
Rust 是一种静态类型语言,这意味着编译器必须准确获知代码中每个变量的数据类型。
-
bool:true or false
-
char:utf-8 编码的 Unicode 码位,是32位的无符号整数。
-
str 和 String:一般使用 &str 形式指向不可变字符串数据的指针,而 string 是增长的、可变的并且在堆上分配的字符串类型。
-
数字类型:Rust 通过类型注释来识别整数。
-
数组:Rust 在显示声明数组类型的同时也要声明元素数量let x: [i32; 2] = [1, 2];
-
元组:一种有序且不可变的对象列表,跟数组的区别就是元素类型不必统一。
2.2 函数
函数采用 fn 关键字来定义,与大多数强类型语言相同的是参数值和返回值都需要声明类型,唯一不同的地方是 Rust 中可以省略 return,函数默认返回最后一行表达式的结果。
fn main() { let value = String::from("Adennan"); greet(value);}fn greet(value: String) { println!("Welcome to Gear Factory {}", value);}
2.3 所有权
在管理内存的方式中各种语言有很大的不同,C/C++ 中程序员需要手动分配和释放内存,在 Go 中是实现了垃圾回收的机制,在程序运行时不断地回收不再使用的内存。Rust 则选择所有权的方式管理内存。所有权可以说是 Rust 中最独特的特性,正是由于所有权的概念保证了 Rust 无需垃圾回收即可内存安全。
我们先来看一下所有权的规则:
-
每一个值都有一个被称为其所有者 owner 的变量
-
每一个值在同一时间内有且仅有一个所有者
-
当所有者离开自己的作用域,这个值将会被释放
展开来说在 Rust 中每个变量都有它绑定到的值的所有权。在变量超出作用域时,Rust 就去清理这部分资源。
我们可以通过几段代码来体会 Rust 中的所有权。
fn hello_world() { let s = String::from("Hello World!"); // use s to do something etc. s+}// s is dropped
String 类型是分配在堆内存里的,这也意味着我们需要回收内存。上文中的代码是说在函数的作用域内创建 s 时,字符串中分配的数据会在堆中分配。当函数运行结束后,s 就超出了作用域,此时 Rust 会调用 drop 函数来清理相关的字符串数据。
总结一下只要跳出具有所有权的变量作用域,那么该变量上所拥有的堆内存就会被释放掉。由此就引发了我们的思考,如果不能跨作用域访问,这一点就很像局部变量超出作用域后会被回收的概念。那么在 Rust 中是如何跨域作用域的呢?
**所有权的转移
**
fn main() { // 1. assignment let s1 = String::from("Hello World!"); let s2 = s1; println!("{}", s2); println!("{}", s1); // 2. copy let s3 = String::from("Hello Adennan"); let s4 = s3.clone() println!("{}", s3); println!("{}", s4);}
第一个采用赋值的方式是不能通过编译的,因为 s1 绑定的所有权已经转移到了 s2 身上,导致 s1 不再有效,是已经被废弃的。这就是实现了所有权的转移。
第二个采用复制的方式克隆 s3 的数据传递给 s4,并没有转移 s3 中的所有权。所以 s3 还是可以继续被使用的。
总结一下 Rust 所有权的赋值转移在一方面废弃了原先的变量,在设计上就天然避免了野指针的情况,另一方面赋值转移实现的是浅拷贝,指向底层的堆的数据并没有发生变化。另外做一下补充,下面的这个例子告诉我们如果是通过函数的返回值进行的赋值操作,就将所有权转移到了原作用域之外的变量上去。
fn main() { let s1 = String::from("hello"); let (len, s2) = length(s1); println!("{}, {}", length, s2);}fn length(value: String) -> (usize, String) { return (s.len(), s)}
所有权借用
在所有权转移的讨论中,我们提到通过转移的方式原先变量就被废弃了。如果我们不想让它废弃,想在函数调用后可以继续使用原先变量,就可以使用借用的方式。
fn main() { let s = String::from("Hello World!"); let l = length(&s); // 借用 println!("{}, {}", length, s);}fn length(value: &String) -> usize { return s.len()}
所有权的借用是通过引用实现的。通过借用的方式得到的对堆数据的引用是没有所有权的,所以借用方在离开作用域后,就不会发生内存的释放。借用一般而言有两种,分为可变借用和不可变借用。可变借用是说可以对数据进行修改,反之不可变借用就是不能修改。对于可变借用(mut)和不可变借用,以及由此引申出的数据竞争约束,我们放在后边的文章中再来讨论。
The End
码匠联盟
技术向善 科技兴邦
9篇原创内容
公众号