Rust语言介绍
From Wikipedia:Rust是一个由Mozilla主导开发的通用、编译型编程语言。它的设计准则为“安全,并发,实用”,支持函数式,并发式,过程式以及面向对象的编程风格。Rust的语法与C++相似(我不这么认为),但它在维持高性能的情况下能保证内存安全。
Rust语言官网:Rust 是一种系统编程语言。 它有着惊人的运行速度,能够防止段错误,并保证线程安全。
Rust连续3年在stackoverflow 年度调查中被评为最受喜爱的编程语言(the most loved programming language。)
扩展阅读:为什么我说Rust是靠谱的编程语言 by Liigo
特性
无 GC & 无 VM & 极小运行时 & 完善的包管理 & 高效 C 绑定
Rust是编译型强类型语言,不是JavaScript、Python这种动态类型语言,也不是Java、C#这种需要虚拟机的语言,也不是Golang这种需要GC的语言。Rust的运行效率与C++相同甚至更好,而Rust的设计目标就是替代C、C++。
在过去,如果向要写高性能,高实时性,或者接近底层的大型程序,我们几乎只能选择C、C++。出于开发效率的考虑,应用软件很少用汇编开发,使用纯C的也很少,而操作系统等底层软件更多的使用C语言,并伴随少量的汇编,几乎不会使用C++(似乎只有Google的Fuchsia使用阉割后的C++)。C++从1983年诞生以来,从C with classes,到C with template,再到C++11,编程范式几经改动,存在许多奇技淫巧,却始终摆脱不了C的影子,就像一辆50年前的跑车经过多次改装,从里到外都改成了21世纪的超跑,虽然开着炫酷,但很容易出故障。而Rust没有历史包袱,其设计上比c++科学的多,没有C++满身补丁的痕迹。
Rust的运行时很小,为了底层开发甚至操作系统开发,减小平台移植难度,Rust在开发阶段砍掉标准库std中的许多重型功能,并建设了Rust包管理软件cargo,包托管网站crates.io,包文档的托管网站docs.rs,其中的许多包的质量和std一样高,使用也很方便。但Rust的std又不像C的库一样缺少基础设施,hashmap、统一api的多线程、网络功能都有。Rust还允许程序不使用std库,只使用平台无关的core库,以进行嵌入式和操作系统开发,也允许手动选择内存分配器(molloc)。
Rust设计上无GC、VM让其很容易调用C代码,但Rust更近一步,实现了Rust与C、C++的高效双向调用。许多语言只能自己调用C库,并且有效率上的问题,但Rust可以和C无缝交流,既可以在Rust项目中使用C库,也能在C项目中使用Rust库。此外Rust也能和python,JavaScript,Ruby进行模块调用。
trait & 泛型 & 模式匹配 & 闭包 & 基于语法的宏 & 零开销抽象
Rust具有泛型,像C++一样,泛型函数会编译成不同类型的版本,而不是像Java一样抹去类型信息。Rust不像Java那样宣传everything is class,没有继承,有结构体并能为其实现方法。Rust 有像Java中的interface一样作用的trait,指定一个结构体需要实现哪些方法,并可以用在泛型中。Rust使用属性来自动实现一些trait,例如,
#[derive(PartialEq, Clone)]
struct Foo<T> {
a: i32,
b: T,
}
编译器会为这个struct自动实现PartialEq, Clone。
impl<T: PartialEq> PartialEq for Foo<T> {
fn eq(&self, other: &Foo<T>) -> bool {
self.a == other.a && self.b == other.b
}
fn ne(&self, other: &Foo<T>) -> bool {
self.a != other.a || self.b != other.b
}
}
impl<T: Clone> Clone for Foo<T> {
fn clone(&self) -> Foo<T> {
Foo {
a: self.a.clone(),
b: self.b.clone(),
}
}
}
Rust广泛的使用了枚举类型,Rust的枚举与C不同,更像是来自函数式编程语言的代数数据类型。最常见的两个枚举就是Option
和Result
enum Option<T> {
Some(T),
None,
}
Option
代表了一种非常常见的场景:一个值要么是一个值,要么什么都不是。许多语言都含有空值null
,但程序并不能像对待非空值那样对待空值。快速排序算法的作者,图灵奖得主,同时也是NULL
的作者Tony Hoare认为他发明的空指针是一个十亿美元的错误。我们要么到处使用xxx!=null
,要么放弃治疗,自以为null不会出现,并放任程序可能某一天不知为什么的崩溃。Rust的Option就能解决这一表达问题。我们可以使用Option的方法提取到Some中的数据T,并根据需要在遇到None时崩溃unwrap()
、返回默认值unwrap_or(T)
、或执行闭包中的回调函数let x = some_u8_option.unwrap_or_else(|| 2 * y)
,|| 2 * y
是一个闭包,代表没有传入的参数,返回2*y,其中y的值应该出现在上文中,或者使用模式匹配:
match some_option {
Some(val) => println!("{}",val),
None => println!("nothing find:("),
}
这样,编译器与程序员都可以清楚的知道何时会出现这种情况,并选择一种解决方案。
enum Result<T> {
Ok(T),
Err(Error),
}
Result
则代表了另外一种常见情况:异常处理。Rust不使用try,catch这种处理方式,而是选择使用返回Result枚举,如果返回的是Ok,那么就Ok,如果是Error,那就需要处理错误,或者将错误再次返回——效果和try相同。
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
这么写很繁琐,Rust有专门的操作符来简化返回Err的语法
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
以上两个函数的功能相同。你可能注意到函数最后没有使用return,因为Rust的语句是具有返回值的,我们无需在最后一行写return。_
代表我们对这个表示符的名字不感兴趣。
Rust也可以使用let进行匹配,例如let (x,y)=(1,2)
,或者if let Some(3) = some_u8_value { println!("three"); } else { println!("not three"); }
Rust具有强大的宏系统。C语言的宏是基于文本替换的,很容易出现替换问题,而Rust的宏是基于语法单元进行展开的。宏可以自动生成代码。例如
macro_rules! find_min {
($x:expr) => ($x);
($x:expr, $($y:expr),+) => (
std::cmp::min($x, find_min!($($y),+))
)
}
这个宏通过递归实现了寻找参数中最小值,普通的函数并不能接受任意个参数,而println!,pacnic!这样的宏就能接受任意多的参数。Rust的宏系统还没有完成,相关信息:RFC,Issue
避免段错误、悬垂引用、线程数据竞争,保证内存安全
上面的特性听起来像Rust是重新造C++的轮子,只是语法不同,但这个特性成为超越C++的关键。写过C或C++的一定遇到过内存安全问题,这些各种各样奇怪的指针问题,常常要调试数个小时甚至数天,有时程序莫名其妙的崩溃,有时程序运行正常但含有安全漏洞,许多漏洞都是缓冲区溢出等等内存不安全问题。C发明时,计算机处理能力有限,内存也很有限(比现在几块钱的单片机还差),当时的共识是程序员有能力管理好内存。然而几十年过去了,内存从比尔盖茨口中的“640k的内存能做任何事情”到了几个GB,程序员在面对几十万行的代码时还能管理好内存吗?
指针的存在就是为了访问指针指向的数据,由于C将内存数据的生命周期管理交给了程序员,因此C程序员必须手动使用指针、molloc和free维护数据的生命周期,然而人总会写出use after free的错误,因此Java、.NET、JavaScript、Python、Golang都使用垃圾回收算法来自动管理内存,然而,运行时也是人写的,Java runtime、IE、Flash这些平台运行时本身就是BUG大户,占据0day安全漏洞数量排行前3的地位。C++11之后,智能指针等特性让内存安全问题更不容易出现,但由于兼容性等原因,C++始终不能放弃裸指针的使用,也就不能保证安全,只能减少问题。
Rustc禁止在unsafe外使用裸指针,也禁止产生空指针。Rust使用一套简单的概念,让Rustc编译器维护了类型的生命周期的问题。首先,Rust中的变量只有标识符的作用,不包含值,只有将资源绑定到标识符上变量才可以直接使用。例如let x:i32=100
,就是将100这个i32的值绑定到x这个标识符上,如果我们只声明let x:i32
,再试着打印x,编译器就会报错,而许多语言会使用默认值甚至是内存里的随机值。Rust的作用域为大括号级别的,如果局部变量离开作用域,会导致其绑定的资源销毁(drop),这是编译器自动插入并执行的,类似C++的析构,但我们不需要手动编写drop函数,这是通过所有权系统实现的。一个资源,无论是栈上的还是堆中的,都只能绑定到一个变量,即这个变量拥有了这个资源的所有权。只有拥有资源所有权的变量离开作用域了,资源才会被销毁。一个资源对应一个变量,Rustc在编译时就可以确定所有资源的生命周期,即从其创建到最后一个拥有它的变量离开作用域,这套系统也被称为RAII(Resource Acquisition Is Initialization,资源获取就是初始化),如果我们在生命周期外使用资源就会在编译时报错,而C++则是运行时。
Rust中的绑定默认是immutable的,如果需要修改值,需要在声明时使用mut
关键字,例如let mut x:i32=100
。Rustc会追踪声明为mut
但没有修改过的变量,并给出warning。
有时候我们会把一个值赋给另一个变量,例如let y=x
(Rust会自动判断类型),这时x的所有权会转移(move)给y,我们再试图访问x就会报错。不过,对一个类型实现了clone这个trait后,这个类型的数据在move时会clone自己再赋值到新变量上,原变量仍然可以访问,大多数栈上的原始类型都实现的clone。
如何在不获得所有权的情况下访问一个不能clone的资源呢,Rust提供了借用。借用(borrowing)也叫引用(reference),但与其他语言不同,在同一个作用域中,只能出现一个可变引用,如 let y = &mut x
(x必须是可变的),或多个不可变的引用,如let y = &x; let z = &x
。借用不会转移所有权,因此函数如果不想转移所有权,就必须将参数声明为引用。借用的生命周期必须处在所有者的生命周期内,否则会出现编译错误。存在借用时,所有者不能更改资源,也不能转移所有权。整个借用系统的检查是在编译时完成的。
有些时候,Rustc不能推断出函数的参数与返回值之间的生命周期关系,例如fn foo(x: &str,y: &str) -> &str
,Rustc不知道返回值来源于x还是y,会指出:
error: missing lifetime specifier [E0106]
fn foo(x: &str, y: &str) -> &str {
^~~~
我们可以修改为fn foo<'a>(x: &'a str, y: &'a str) -> &'a str
,'a
代表生命周期,表示如果x,y都在a这个生命周期下,返回值就一直有效,或者指明fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str
y的生命周期更长,因此返回值的生命周期至多为x的生命周期。
生命周期语法在新手看来非常复杂,相比起C++手动管理生命周期,写几个标记并不是难事。Rust开发人员正在努力的减少无法推断的生命周期类型。
有时候,Rust的安全检查阻止我们实现一些功能,例如编写操作系统的内存管理模块,我们需要指针这样的功能。Rust含有unsafe
关键字,在unsafe的代码块下,Rust额外可以:
解引用裸指针 调用不安全的函数或方法 访问或修改可变静态变量 实现不安全 trait
裸指针忽略借用规则,且允许为空,unsafe需要我们自行判断裸指针是否指向合法内存,需要手动free裸指针的内存。我们可以通过放弃Rust提供的安全保证换来对其他语言或硬件的访问。注意,unsafe不代表不安全,只是说内存安全需要由程序员维护。
C语言/C++发明时CPU还都是单核心,而现在CPU核心数越来越多,再加上超线程技术,应用程序势必要走向多线程,然而竞争的存在使得使用C++编写程序很容易出现bug。Servo是Mozilla开发的多线程并行浏览器引擎,也是唯一一个并行浏览器引擎,为什么是唯一一个呢,因为使用C++根本没用办法写出没有bug的并行浏览器来。(这里的并行指的是每一个网页的解析、渲染、运行是并行的,大多数浏览器是每个网页一个进程,但这个进程的并行度不高) Servo的CSS渲染引擎成为Firefox57的默认引擎后,其CSS渲染性能提升100%以上(技术细节)。
Rust通过Send+Sync
这两个trait处理多线程问题。这两个trait没有定义任何方法,Send
表示为如果T:Send
,将T move到另一个线程,能保证所有权能安全的转移到新的线程并与原线程解耦,不会使原线程use after free或可以同时访问同一块内存等内存安全问题。Sync
表示为如果T:Sync
,将&T send到另一个线程,不会出现竞争问题。显然,T:Sync
等于&T:Send
,Sync+Copy=Send
,send
和sync
不能手动实现,基本上所有的原始类型都是Send+Sync
的,由原始类型与结构体、枚举组合成的类型也是Send+Sync
的,而且是自动实现的,无需写任何代码。但标准库中有一些类型不是Sync
或Send
的,它们的底层使用了unsafe的Rust(按照Rust的内存安全措施写不出这些类型)。裸指针既不是Send
也不是Sync
,手动实现Send和Sync需要unsafe块,但我们可以指定一个类型为!Send
或!Sync
,无需unsafe。尽管Rust标准库和第三方包中使用了许多多线程API,但其中的安全性都是基于这套系统的。
大事件
- 2006年Mozilla员工Graydon Hoare基于OCaml发起了Rust项目.
- 2009年,Mozilla开始赞助这个项目。
- 2010年,Rust编译器源码从OCaml转到Rust,次年完成自举,编译器被命名为Rustc,后端为LLVM。
- 2012年,Rust首个大型项目,由mozilla和三星发起的Servo浏览器引擎开始编写,其目标是创造一个安全的并行浏览器引擎。Rust与Servo项目相互发展,共享一部分开发者。
- 2012年,Rust 0.1发布,基本语言功能完善。
- 2014年,分离core库与std库,以面向嵌入式与操作系统开发。
- 2015年,Rust放弃绿色线程与垃圾回收库,删除运行时库。
- 2015年,经过12个0.x,2个alpha版本,Rust1.0.0发布,这3年内每个版本改动均在一千以上。Rust承诺在2.0之前不破坏API兼容性。
- 2016年,Servo发布预览版0.0.1。
- 2017年,Servo部分代码进入Firefox正式版,如CSS渲染引擎Stylo
著名项目
- Rustc,编译器
- servo,并行浏览器内核
- redox,纯Rust编写的GUI类unix微内核操作系统
- tokio,事件驱动的非阻塞网络IO库
- hyper,Web框架
- gutenberg,类似hexo的静态博客
- rustls,安全的TLS库
- way-cooler,wayland窗口合成器
- ripgrep,最快的文本搜索命令行程序
- stdweb,客户端JavaScript/WebAssembly框架
- xray,下一代atom编辑器
- xi-editor,Google员工发起的前后端分离的编辑器
- alacritty,GPU加速的终端模拟器
- tikv,数据库(TiDB的一部分,国产)
- piston,游戏引擎
- tockos,嵌入式操作系统
使用Rust的商业项目见Rust官网的页面