简介
Rust 是最近几年开始兴起的编程语言,虽然目前还没看到要像 Go 一样”大火“的趋势。但是,官网的一些 featuring 看着就很让人心动(虽然还不知道现实如何~)。
Rust 的官网介绍:
Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.
Featuring
- zero-cost abstractions
- move semantics
- guaranteed memory safety
- threads without data races
- trait-based generics
- pattern matching
- type inference
- minimal runtime
- efficient C bindings
零抽象开销、高效的 C 语言绑定、内存安全、线程安全…看起来就是一个现代 C++ 的加强版。
工具链
目前, Rust 的开发速度很快,保持着每 6 周更新一次的节奏。Rust 发布的工具链包括了 stable
、beta
和 nightly
三种不同版本。 nightly
是最激进的版本,包含了大量(可能不稳定)的新/高级特性。stable
版本目前可能还不支持一些高级特性。beta
介于两者之间。
rustup
因为 Rust 的更新速度很快,支持的版本很多,有时新版本是不会完美兼容旧版本的,同时还支持多平台交叉编译。所以就有了 rustup 这个 Rust 工具链的管理工具。
安装
安装 Rust 就是从安装 rustup 开始,很方便,可以参考这个页面。
介绍
关于 rustup 的介绍和使用,具体可以参考这个页面。
# 更新工具链
rustup update
# 更新 rustup 本省
rustup self update
# 安装最新的 nightly 版本
rustup install nightly
# 指定版本
rustup install nightly-2018-07-18
# 指定默认使用的版本
rustup default nightly
cargo
使用 rustup 安装 Rust 时,主要安装了:
- rustc - 编译器。
- rust-std - 标准库。
- cargo - 包管理工具。
- rust-doc - 说明文档。
其中,最重要是 cargo 这个包管理工具,rustc 编译器的使用,大部分也是通过 cargo 来调用的。
cargo 是一个 Rust 下一个十分强大的工具,详细信息可以参考 The Cargo Book 。
宏(macros)
第一个 Rust 程序,著名的 Hello World
系列(参考自这个页面)。
fn main() {
println!("Hello, world!");
}
打印一句Hello, world!
,然后换行,多简单啊!
但是看到 println!
那个感叹号时,我的强迫症要爆发了 —— 这个“函数名”为什么要多一个感叹号?。?!
往下看,发现 xyz!
这种东东在 Rust
中叫做 宏(macros)
。
C++ 里面也有宏,从 C 那里继承过来的。对 C++ 来说,其实宏的能力很弱小 —— 虽然可能通过各种奇技淫巧、很费劲地写出功能强大的代码,但是相对地也会大大降低代码的可读性和可维护性。(C++ 中复杂的宏,估计过几个月,原作者都不敢随便改动了…直接的字符串替换,在不确定用户使用场景的时候,非常容易出问题。)
Rust 中的宏功能强大、严谨很多。
Fundamentally, macros are a way of writing code that writes other code, which is known as metaprogramming.
我只是看了一下文档,没真正写过 Rust 的宏,有兴趣的可以看看这个官方文档——Appendix D: Macros。
返回值和错误处理
在 Rust 的世界里,错误分成两种 recoverable 和 unrecoverable 。
- recoverable error,比如“打开一个不存在的文件”。
- unrecoverable error,比如“访问数组越界”。
unrecoverable
遇到 unrecoverable error 时,程序会调用 panic!
宏,然后进程就挂了??戳艘幌挛牡?,很简单,具体可以参考这个页面。
recoverable
写 Rust 代码时,遇到比较多的是 recoverable error。下面简单说一下,主要内容来自这个页面。
recoverable error 通过函数的返回值来表示。这一点,Rust 和 Go 一样,都抛弃了 exception 风格的错误处理方式。
不同的是,Go 通过多个函数返回值来返回数据+错误信息,Rust 则搞了一个一开始看起来比较奇怪的返回值 —— Result<T, E>
。
Result<T, E>
的定义如下:
enum Result<T, E> {
Ok(T),
Err(E),
}
使用示例:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(ref error) if error.kind() == ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => {
panic!(
"Tried to create file but there was a problem: {:?}",
e
)
},
}
},
Err(error) => {
panic!(
"There was a problem opening the file: {:?}",
error
)
},
};
}
作为一个 Rust 的初学者,我觉得 Rust 这样的返回值,错误处理的代码看起来一点都不清晰,有点繁琐…
可能是 c++ 写多了,个人还是比较习惯 Go 那种多个返回值的错误处理方式,虽然 C++ 不支持多个返回值。
为了简化 Rust 的错误处理代码,Restlt<T, E>
实现了一些错误处理的封装:unwrap
和 expect
。
unwrap
If the
Result
value is theOk
variant,unwrap
will return the value inside theOk
. If theResult
is theErr
variant,unwrap
will call thepanic!
macro for us.
使用示例:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
expect
expect
, which is similar tounwrap
, lets us also choose thepanic!
error message. Usingexpect
instead ofunwrap
and providing good error messages can convey your intent and make tracking down the source of a panic easier.
使用示例:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
the ?
Operator
有时候,我们写一个函数,只想把更底层的错误直接传递给上一层。
代码示例:
use std::io;
use std::io::Read;
use std::fs::File;
fn read_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),
}
}
上面的代码封装了一个直接读取文件数据的函数 read_from_file
。当打开文件或读取文件数据出错时,我们希望把错误传递给调用方,而不是直接 panic!
,所以不能使用 unwrap
或 expect
。像最原始那样写错误处理代码可以解决这个问题,但是代码也显得很繁琐。
所以,Rust
有提供了一个语法糖—— the ?
Operator。上面的例子可以改写成:
use std::io;
use std::io::Read;
use std::fs::File;
fn read_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)
}
进一步简化上面的代码:
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
Ownership 和生命周期
常见的内存管理方式有两种:
- 一种是使用 Garbage Collection,如 Java、Go。
- 另一种是由开发者主动分配和释放内存,如 C++。
- 而 Rust 采用了第三种方式,通过 Ownership 这个特性,可以在编译器对内存的管理进行检查,实现了不需要垃圾回收的内存安全保证(应该主要是保证不发生内存泄漏)。
Ownership 的规则很简单:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
简单地说,就是一块内存,有且只有一个 owner,当这个 owner 离开它的作用域时,内存就会被回收。由此,Rust 引出了 Move 语义 和 Copy 语义。熟悉 C++ 的人对这个两个概念应该比较熟悉。
默认情况下,一些比较“复杂”的对象的赋值都是 Move 语义。某些情况下不想/不能使用 Move 语义,Rust 就引入了 references —— 本质上是指针,而不是 C++ 里的引用。并且,Rust 限制一个对象最多只有一个 mutable reference 或多个 immutable references。
引入指针,就遇到 dangling references(pointers) 的问题。如何在编译期间检测出 dangling references ?
为了防止出现 dangling references,必须保证 references 指向的对象是有效的,即确保 references 的使用是在对象的生命周期(lifetimes)之内。Rust
引入了一种叫做 Lifetime Annotation Syntax 让开发者说明 references 的生命周期,然后在编译期拒绝掉所有生命周期不合法的代码。个人感觉文档说得很有道理,但是又不太好理解,具体可以再看看文档,再研究下其原理。
闭包
闭包其实很简单,概念大部分人应该都懂,只是每种语言都有自己的闭包语法。所以一开始看到 Rust 的闭包代码时,也是摸不清头脑,不知道是在写什么。
Rust 的闭包语法的基本形式是:
|agr1, agr2| { do-something }
当然,这里面又会涉及参数捕获、生命周期等问题。具体看文档吧 —— 介绍闭包的文档。
小结
宏是 Rust 里一个非常重要的特性,代码里的很多地方都可以见到。与 C++ 里的宏不同,Rust 的宏是完全重新设计、被彻底加强过得。
错误处理的代码无处不在,而 Rust 的错误代码写起来又有点“奇葩”,一点都不像在处理错误。
Ownership 和生命周期应该是 Rust 里最普遍、最重要又最难掌握的特性之一。
闭包这个很简单,认识一下语法就行。
写这篇文章,主要是记录一下第一次阅读 Rust 的代码时遇到的一些问题,为 Rust 的代码阅读清扫一下障碍,Rust 真的挺复杂很多,后面都不知道还有多少坑。