Rust 是一门编译型系统编程语言。官方给出的定义是:
Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.
可以看出这是一门主打效率,安全和并发的语言。
Rust 有着接近 C 的效率,具体的对比可以从 Benchmarksgame 上看到。有人说这会是一门替代 C/C++ 的语言,这一点很难说,但是可以肯定这是一门很不错的语言,绝对值得一学。
在安全方面,更多的是本身的设计做保障,生命周期,所有权等,这些下面会详细介绍。基本上,如果你没用到 Rust 中那些不够安全的东西的话,所有的安全问题都可以在编译阶段被检查出来,防止运行中发生问题。
Rust 的无畏并发,得益于本身的设计解决了大部分数据读写的冲突,使得并发的效率惊人的高。
下面很多代码都是官方的文档中复制过来的,挑的都是根据这篇介绍就可以看懂的。
出身
Rust 是由 Mozilla 主导开发的,1.0 版本于 2015 年 5 月 15 发布,是一门非常年轻的语言,但是它有着相当友好的社区支持,非常棒的错误提示,优秀的设计理念,即使有着相对较高的门槛,也深受开发者喜爱,在 StackOverflow 2016 survey StackOverflow 2017 survey 上都是最受喜爱的语言,也许高门槛帮它过滤了很多不必要的负面的东西吧。
另外,Rust 的吉祥物是一只橙色的螃蟹,大概是生锈的螃蟹?
应用
Mozilla 首先利用 Rust 开发了一个浏览器引擎 servo ,Firefox Quantum 的崛起正是得益于 Rust 的高性能和并行特性。
既然是系统编程语言,少不了有人用它写操作系统,这便是 Redox OS ,目前已经是一个有着诸多功能的操作系统,感兴趣的可以安装试玩。这个操作系统也用到了新的文件系统 TFS 。
Google 有一个文本编辑器 xi-editor ,后端就是用 Rust 写的,目前也有几个对应的前端,不过都算不上很成熟。
目前的命令行工具有 fd ,exa ,ripgrep ,分别对应 find
,ls
,grep
命令,值得一试。
机器学习这么火,Rust 也有相应的库 Leaf ,不过两年前停止更新了,毕竟这方面 scikit-learn 实在太出色了。
也有一个 Rust 终端 Alacritty ,号称是最快的,使用了 GPU 加速,而且是跨平台的。
有人用 Rust 重写了一份 GNU coreutils 工具,利用了 Rust 跨平台编译的特性,可以方便地将这套工具部署在 Windows 上。
目前也有一些 Web 框架,像 Rocket ,听名字就知道性能强悍,微服务框架 pencil ,主打扩展和并发的 iron ,看名字就觉得很 rust。
分布式 key-value 数据库 TiKV ,PingCAP 公司出品,可以对接他们搞的 TiDB 。
区块链方面,有 Exonum 和 CITA ,针对 Ethereum 区块链应用平台还有 Parity 这样的客户端。
游戏引擎方面有 Piston 。
其他项目参考 Awesome List 吧。
语法
Rust 在语法上接近 C/C++,但是关键词的缩写为人诟病,在这个编辑器智能补全技术十分成熟的时代,实在没多少必要。
fn main() {
println!("Hello, world!");
}
变量类型有可变和不可变,这一点和 Scala
类似。
基本变量类型 int
,float
还能细分各种长度,比如 i16
,u32
,f64
等,整型中间的可以用 _
来作分隔符,比如 100_000
等同于 100000
。char 类型则是完全支持 Unicode 的。高级类型,诸如可容纳不同类型的 tuple,同一类型的 array。
函数声明中必须明确指定每个变量的类型和返回值的类型。需要注意的是, Statements 只执行操作而没有返回值,Expression 会计算并给出返回值。函数中如果需要返回一个变量,只需要在函数末尾给出相应的表达式并不加 ;
即可:
fn add_one(x: i32) -> i32 {
x + 1
}
控制流中需要的 bool
类型必须为 bool
类型,而不能使用诸如 0
,1
,[]
等代替。
Cargo
Cargo 是 Rust 的一个包管理工具,不过它能够做的不仅仅是包管理。
(你可以在 crate 上找到 Rust 的所有包,相当于 Python 的 PyPI 平台)
Cargo 的功能非常丰富,如果拿 Python 来作对比的话,大概相当于 pip
+ setuptools
+ pipenv
+ twine
等工具的总和。
Rust 项目通常都是从 cargo new project_name --bin
开始的。此时会自动生成一个 Cargo.toml
文件,这里面包含项目的信息,以及作者资料,这部分从 Git 中获得,而且会自动执行 git init
。当运行 cargo run
之类的 build 命令时,就会生成 Cargo.lock
文件,其中是项目的依赖,是由 Cargo 根据实际项目生成的,不应该被人为修改。
Cargo 同样可以用来执行测试,前提是你写了相关的测试,不管是专门的测试代码,还是包含在注释中的测试代码,都可以被执行。
另外,Cargo 还可以根据注释快速生成文档,并且提供了相应的 Web 服务,可以说是非常方便了。
其他特征可以参考 Cargo 文档 。
Trait
trait 有一点类似于其他语言中的 interface ,用于定义类型的一些通用行为。
pub trait Summarizable {
fn summary(&self) -> String;
}
这是一个简单的定义,没有给出 summary
方法的默认实现。之后,就可以应用到需要的地方去了。
pub struct News {
pub headline: String,
pub author: String,
pub content: String,
}
impl Summarizable for News {
fn summary(&self) -> String {
format!("{}, by {} \n{}", self.headline, self.author, self.content)
}
}
像上面这样,可以将 Summarizable
用到任何需要的地方,这也是利用 Rust 实现 OOP 的重要工具。
Object Oriented Programming
面向对象是一种很常见的模式了,对 Rust 而言,它属于“薛定谔的面向对象”。
一方面,Rust 拥有结构体 struct
和枚举 enum
以及 impl
方法,理论上讲可以实现类似于面向对象程序设计中的一个对象包含的数据和方法。而 Rust 又是非常谨慎的,不标明 pub
的都是私有数据和方法,因此可以封装实现细节,外部仅能访问 pub
类的数据和方法。
另一方面,Rust 是不能直接继承的,而是通过为结构体定义通用的 trait
方法,这有点类似于动态语言中的鸭子类型的概念。至于多态,同样可以利用 trait
做到。
总的来说,Rust 并不是一门纯粹的 OOP 语言,不过你喜欢 OOP 的话也可以用得起来。但是 OOP 有时候并不是那么 “Rustician” 。
Ownership & Garbage Collection
Rust 是没有垃圾回收机制的,相应的,为了保证内存安全,它有着独一无二的 ownership 。每一个变量都对应有它的 owner,且在任何时候都只能拥有一个 owner (这保证了多线程中的数据安全),当 owner 离开作用域的时候,值就会被丢弃,对应的内存空间就会被释放。可以这样理解,Rust 的垃圾回收是做到极致了,你必须清楚自己用到的每一块内存在什么时候会废弃,而 Rust 就像一个旋涡,一旦你松手,内存就会被它吸进去回收掉。因此,你需要考虑的不是什么时候释放这块内存,而是要把这块内存保留到什么时候。
对于基本类型,当你使用 x = y
这样的语句时,y
的值会被复制给 x
,他们属于拥有相同值的两个变量。而对于复杂类型,类似 C/C++ ,这样只会把指针内容复制过去,而数据是保持不变的,同时数据的 owner 变为 x
,此时调用 y
则会出错,因为它已经不指向任何内存了。如果你想把数据也复制过去,就需要调用对应的 clone
函数了。
Rust 中给函数传递复杂类型需要用到引用 &
,除非你想放弃该变量的 ownership 。引用允许函数访问数据但没有 ownership ,权限默认为可读,如果你加了 mut
,则函数可读写借到的数据,当然这要求变量本身是可变类型的,毕竟地主家里都没有余粮,怎么可能借给租户呢?
Functional Programming
Rust 的闭包语法类似于 Smalltalk 和 Ruby,有类型推断和注解,这意味着你不必写全类型,但是用过一次之后便会锁定类型不能再改变了。另外,Rust 的闭包还能够捕获环境变量,这是函数不具有的。
fn main() {
let x = 1, z = 2;
let equal2x = |y| y == x;
assert!(equal2x(z));
}
Rust 的迭代器跟 Python 一样是惰性的,本身的实现就是不断调用 .next()
方法,同时可以配合闭包创建新的迭代器。
fn main() {
let x = Vec<i32> = vec![1, 2, 3];
let y = Vec<_> = x.iter().map(|x| x + 1).collect();
assert!(y, vec![2, 3, 4]);
}
Rust 的迭代器跟 C++ 一样是零成本抽象的,实际中可能还要比普通的遍历快,所以尽可能使用迭代来提高效率,简化代码吧。
Lifetime
Rust 中所有的引用都有一个生命周期,通常是可以由编译器推断的,不过有些时候需要我们自己来规定,这大概是 Rust 和其他编程语言最不同的地方了。
let r;
{
let x = 5;
r = &x; // Error
}
println!("r: {}", r);
编译器中负责这一部分的叫做借用检查器 borrow checker,不要小看它,这可能是 Rust 中最难以深入理解的一部分了。对于每一个变量,借用检查器都会默认给出一个生命周期注解,通常以 '
开头,比如 'a
,当借用检查器发现一个变量引用了一个比自身生命周期小的变量时,就会报错,这里的小是指不能够包含本身。
生命周期是需要能够明确推断出或者被明确指定的,这一点可以严格保证数据安全。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
大家一定会觉得 Rust 疯了,这么简单的功能需要写的这么复杂吗?是的,很必要。Rust 需要知道生命周期准确的范围,即使像上面这样无法知道准确两个变量的准确范围,那就给他们加上泛型生命周期,保证他们都会在某个特定的作用域中。到这里你会觉得这门语言真的有点麻烦呢,原以为用上 Rust 之后就不必像 C++ 那样到处考虑指针的问题了,但其实 Rust 的生命周期的复杂度也非常高,从门槛上来讲,或许要比 C++ 还复杂。因为你需要实现精准的控制,而不是大概好像就那样吧。
Smart Pointer
Box<T>
是最简单的智能指针,指针本身存在于栈上,数据则保存在堆中,这一点其实都差不多。Box 是允许创建递归类型的,不过 Rust 本身要求大小必须是明确的,否则存在不安全隐患。
Deref
可以重载 *
解引用运算符,Drop
用来清理代码(Rust 本身会在离开作用域时执行这个),Rc<T>
可以存放引用计数,允许对该类型进行不完全拷贝,来使数据同时有多个所有者,当然这个数据是不可变的。对于可变数据,就需要用到 RefCell<T>
了。这部分仔细讲起来太占地方了,而且我也没有在实际中用过,恐怕写不好,也没有想到什么通俗的例子,大家还是看 文档 去吧。
其实写到这里,多少能感受到 Rust 的门槛了。
Concurrency
Rust 标准库的线程模型是 1:1 的,即一个 OS 线程对应一个语言线程,这是为了效率考虑的,如果你希望更好的控制线程并减少上下文切换成本,那可以用相应的 crate 中的实现。
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join();
}
上面的例子传递了一个闭包给其他线程执行,最后等待所有子线程结束后再结束主线程。
线程中利用 channel 来进行通信再常见不过了,下面是一个简单的例子,其中也包含着所有权的转移。
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap(); // the ownership of val is send to rx
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
另外,对于互斥锁,Rust 有专门的 Mutex
,lock
之后会造成阻塞,当离开作用域的时候会自动释放锁。也有专门的原子引用计数 Arc
,当然这些都跟 smart pointer 有关了。想要在线程之间共享变量,势必会存在所有权的问题,所以需要引入引用计数,从而可以 clone
锁,当然这是一种特殊的不完全拷贝。
use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = counter.clone();
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
因为 Mutex
提供了内部可变性,即使 counter
不可变,我们依然可以通过获取其内部值的可变引用来实现计数。就好像 Python 中 tuple
是不可变的,但是如果是对 tuple
内部的 list
操作就没问题了。
此外,Rust 中还有两个重要的并发 trait Send
和 Sync
,前者用来表示所有权可能被传递给其他线程,后者表示多线程访问是安全的。
Unsafe
Rust 并不是什么都绝对安全,比如上面提到的 Smart Pointer 中的 RefCell<T>
,在编译阶段并不能保证通过就是安全的,而需要用户自己分析。
The Case for Writing a Kernel in Rust 这篇论文就是关于如何在保证高性能的同时实现一个安全的系统内核,其中当然要处理很多不安全问题,尽可能将不安全的模块封装起来,保证特定情况下的绝对安全,感兴趣的可以看看。
Python?
有了如此强悍的性能和安全性,自然想到,应该用到那些需要性能的地方去,还要有友好的借口给其他语言用。
Speed Python Using Rust RedHat 的这篇博客写的就很到位了,这里用到了 rust-cpython 工具,通过 benchmark 可以看出来,Rust 的优势还是非常明显的,几乎是 pure Python 的 100 倍,跟 C 的实现相比,基本上是一个水平的。
当然,目前看来这些工具还是有很大的改进空间的,不过我还是很看好未来的发展的。
总结
如果要用一个词来评价 Rust,我觉得还是 brilliant 比较合适。它并非用了什么 magic ,只是针对它想解决的问题,走了一条特立独行的路,并合理吸收各种编程语言和工具的长处,从而发展出自己一套特有的哲学,可谓是增之一分则太长,减之一分则太短。
本文也只是浅显地介绍了一些东西而已,我也没有拿 Rust 写过什么能拿得出手的东西,都只是停留在 TODO list 上。至于底层的一些实现,那更不是我现在能说得清的。等我深入实践过之后,再来写点感悟吧。