UMBRELLA

未雨绸缪,举重若轻

UnicodeASCII 匹配所有 ASCII 字符,从 00x7f。例如,都将字符 * 分配给码点 42。类似地,Unicode00xff 分配给与 ISO/IEC 8859-1 字符集相同的字符,用于西欧语言的 8ASCII 超集。Unicode 将此码点范围称为 Latin-1 代码块。

因为 UnicodeLatin-1 的超集,所以从 Latin-1 转换到 Unicode 是完全允许的:

1
2
3
fn latin1_to_char(latin1: u8) -> char {
latin1 as char
}

假设码点在 Latin-1 范围内,反向转换也很简单:

1
2
3
4
5
6
7
fn char_to_latin1(c: char) -> Option<u8> {
if c as u32 <= 0xff {
Some(c as u8)
} else {
None
}
}

RustStringstr 类型都是使用 UTF-8 编码格式,它是一种变长编码,使用14个字节对字符进行编码。有效的 UTF-8 序列有两个限制。首先,对于任何给定码点,只有最短的编码被认为是有效的,也就是不能花费4个字节来编码一个适合3个字节的码点。 此规则确保给定代码点只有一个 UTF-8 编码。其次,有效的 UTF-8 不得编码为 0xd8000xdfff 或超过 0x10ffff 的数字:这些数字要么保留用于非字符目的,要么完全超出 Unicode 的范围。

阅读全文 »

Rust 标准库包含几个集合,用于在内存中存储数据的泛型类型。我们已经在前面使用了集合,例如 VecHashMap。在本章中,我们将详细介绍这两种类型的方法,以及其他6个标准集合。

Rust 一共有8个标准集合类型,它们都是泛型:

  • Vec<T>:一个可增长的、堆分配的 T 类型值数组;

  • VecDeque<T>:与 Vec<T> 类似,但更适合用作先进先出队列,它支持在列表的前面和后面有效地添加和删除值;

  • BinaryHeap<T>:一个优先队列,BinaryHeap 中的值是有组织的,所以它总是有效地找到并删除最大值;

  • HashMap<K, V>:键值对表,通过键查找值很快,item 以任意顺序存储;

  • BTreeMap<K, V>:与 HashMap<K, V> 类似,但它保持entries按键排序。 BTreeMap<String, i32> 以字符串比较顺序存储其entries。除非需要entries保持排序,否则 HashMap 更快;

  • HashSet<T>:一组 T 类型的值。添加和删除值很快,查询给定值是否在集合中也很快;

  • BTreeSet<T>:与 HashSet<T> 类似,但它保持元素按值排序。 同样,除非需要对数据进行排序,否则 HashSet 更快;

阅读全文 »

Rust 用于输入和输出的标准库功能围绕三个Trait组织:ReadBufReadWrite

  • 实现 Read 的值具有面向字节的输入的方法,他们被称为 Reader

  • 实现 BufRead 的值是缓冲读取器,它们支持 Read 的所有方法,以及读取文本行等的方法;

  • 实现 Write 的值支持面向字节和UTF-8 文本输出,它们被称为 Writer

在本节中,将解释如何使用这些Trait及其方法,涵盖图中所示的读取器和写入器类型,并展示与文件、终端和网络交互的其他方式。

ReadersWriters

Readers 是内容输入源,可以从哪里读取字节。例如:

Writers 是那些你可以把值写入的地方,例如:

  • 使用 std::fs::File::create 创建的文件;

  • 基于网络连接 std::net::TcpStream 传输数据;

  • std::io::stdout()std::io:stderr() 可以用于向标准输出和标准错误写入内容;

  • std::io::Cursor<Vec<u8>> 类似,但允许读取和写入数据,并在vector中寻找不同的位置;

  • std::io::Cursor<&mut [u8]> 和上面的类似,但是不能增长内部的 buffer,因为它仅仅是已存在的字节数组的引用;

由于ReaderWriter有标准的 Traitstd::io::Readstd::io::Write),编写适用于各种输入或输出通道的通用代码是很常见的。 例如,这是一个将所有字节从任何读取器复制到任何写入器的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::io::{self, ErrorKind, Read, Write};

const DEFAULT_BUF_SIZE: usize = 8 * 1024;

pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64>
where
R: Read,
W: Write,
{
let mut buf = [0; DEFAULT_BUF_SIZE];
let mut written = 0;
loop {
let len = match reader.read(&mut buf) {
Ok(0) => return Ok(written),
Ok(len) => len,
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
};
writer.write_all(&buf[..len])?;
written += len as u64;
}
}

这是 Rust 标准库 std::io::copy() 的实现,因为它是泛型的,所以可以把数据从 File 复制到 TcpStream,或者从 Stdin 到内存中的 Vec<u8>

阅读全文 »

Rust 提供了一种非常好的并发使用方法,它不强制所有程序采用单一风格,而是通过安全地支持多种风格,并由编译器强制执行。我们将介绍三种使用 Rust 线程的方法:

  • Fork-join 并行;
  • 通道(Chanel);
  • 共享可变状态;

在此过程中,将使用到目前为止所学的有关 Rust 语言的所有内容,Rust 对引用、可变性和生命周期的关注在单线程程序中足够有价值,但在并发编程中,这些规则的真正意义变得显而易见。

Fork-Join Parallelism

最简单的用于多线程的案例是处理互不相干的任务,例如,我们要处理大量的文档,可能会这样写:

1
2
3
4
5
6
7
8
fn process_files(filenames: Vec<String>) -> io::Result<()> {
for document in filenames {
let text = load(&document)?; // read source file
let results = process(text); // compute statistics
save(&document, results)?; // write output file
}
Ok(())
}
阅读全文 »

Rust 语言支持宏,如我们之前使用的 assert_eq!println! 等。宏做了函数不能做的一些事情,例如,assert_eq! 当一个断言失败时,assert_eq! 生成包含断言的文件名和行号的错误消息,普通函数无法获取这些信息,但宏可以,因为它们的工作方式完全不同。

宏是一种简写,在编译期间,在检查类型和生成任何机器代码之前,每个宏调用都会被扩展。也就是说,它被一些 Rust 代码替换。assert_eq! 调用扩展为大致如下:

1
2
3
4
5
6
7
8
9
10
match (&gcd(6, 10), &2) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
panic!(
"assertion failed: `(left == right)`, (left: `{:?}`, right: `{:?}`)",
left_val, right_val
);
}
}
}

panic! 也是一个宏,它本身扩展为更多的 Rust 代码。该代码使用到了另外两个宏:file!()line!()。 一旦 crate 中的每个宏调用都被完全展开,Rust 就会进入下一个编译阶段。

在运行时,断言失败看起来像这样:

thread 'main' panicked at 'assertion failed: `(left == right)`, (left: `17`, right: `2`)', gcd.rs:7

如果熟悉 C++,可能对宏有过一些不好的体验。但是 Rust 宏采用不同的方法,类似于 Scheme 的语法规则。与 C++ 宏相比,Rust 宏可以更好地与语言的其余部分集成,因此更不容易出错。宏调用总是标有感叹号 !,因此在阅读代码时它们会比较突出,所以不会意外调用它们。Rust 宏从不插入不匹配的括号或圆括号,并且 Rust 宏带有模式匹配,使得编写既可维护又易于使用的宏变得更加容易。

在本节中,我们将通过几个简单的例子来展示如何编写宏。但与 Rust 的大部分内容一样,理解宏需要下很大功夫。在这里将介绍一个很复杂的宏的设计,它可以将 JSON 文字直接嵌入到我们的程序中。但是宏的内容涵盖的非常多,因此这里将提供一些进一步研究的建议,包括我们在此处展示的高级技术,以及称为过程宏的更强大的工具。

阅读全文 »

系统编程的秘密乐趣在于,在每一种安全语言和精心设计的抽象之下,都存在着极其 unsafe 的机器语言和小技巧,我们也可以用 Rust 来写。

到目前为止,我们介绍的语言可确保程序通过类型、生命周期、边界检查等完全自动地避免内存错误和数据竞争,但是这种自动推断有其局限性,有许多有价值的技术手段是无法被 Rust 认可的。

unsafe 代码告诉 Rust,程序选择使用它无法保证安全的特性。通过将代码块或函数标记为 unsafe,可以获得调用标准库中的 unsafe 函数、解引用 unsafe 指针以及调用用其他语言(如 CC++ )编写的函数以及其他能力。

这种跳出安全 Rust 边界的能力使得在 Rust 中实现许多 Rust 最基本的功能成为可能,就像 CC++ 用来实现自己的标准库一样。 unsafe 代码允许 Vec 有效地管理其缓冲区、 std::io 能直接和操作系统对话、以及提供并发原语的 std::threadstd::sync

本节将 unsafe 功能的要点:

  • Rustunsafe 块在安全的 Rust 代码和使用 unsafe 特性的代码之间建立了界限;

  • 可以将函数标记为 unsafe,提醒调用者存他们必须遵守的额外规范以避免未定义的行为;

  • 裸指针及其方法允许不受限制地访问内存,并允许构建 Rust 类型系统原本禁止的数据结构。尽管 Rust 的引用是安全但受约束的,但正如任何 CC++ 程序员所知道的,裸指针是一个强大而锋利的工具;

  • 了解未定义行为将帮助理解为什么它会产生比仅仅得到错误结果更严重的后果;

  • unsafeTrait,类似于 unsafe 的函数,强加了每个实现必须遵循的规约;

阅读全文 »

世界上的每个程序并非都是用 Rust 编写的,我们希望能够在我们的 Rust 程序中使用许多用其他语言实现的关键库和接口。Rust 的外部函数接口 (FFI) 允许 Rust 代码调用用 C 编写的函数,也可以是 C++。由于大多数操作系统都提供 C 接口,Rust 的外部函数接口允许立即访问各种低级功能。

在本章中,我们将编写一个与 libgit2 链接的程序,libgit2 是一个用于与 Git 版本控制系统一起工作的 C 库。首先,我们使用前一章中展示的 unsafe 特性展示直接从 Rust 使用 C 函数的例子,然后,我们将展示如何构建 libgit2 的安全接口,灵感来自开源 git2-rs。本文假设你熟悉 C 以及编译和链接 C 程序的机制,还假设熟悉 Git 版本控制系统。

现实中确实存在用于与许多其他语言进行通信的 Rust 包,包括 PythonJavaScriptLuaJava。这里没有篇幅介绍它们,但归根结底,所有这些接口都是使用 C 外来函数接口构建的。

阅读全文 »

std::marker::PhantomData 是一个零大小的类型,用于标记一些类型,这些类型看起来拥有类型 T,但实际上并没有:

1
2
3
pub struct PhantomData<T>
where
T: ?Sized;

Rust 并不希望在定义类型时,出现目前还没使用,但未来会被使用的泛型参数,例如未使用的生命周期参数以及未使用的类型。

PhantomData 最常见的用例可能是具有未使用的生命周期参数的结构体,例如,这儿有一个结构体 Slice,它有两个 *const T 类型的指针,可能指向某个地方的数组,我们期望 Slice 类型的值在生命周期 'a 内仅仅有效,但是如果像下面这样,'a 我们又无处安放:

1
2
3
4
struct Slice<'a, T> {
start: *const T,
end: *const T,
}

我们可以使用 PhantomData 告诉编译器就像 Slice 结构包含引用 &'a T 一样来纠正这个问题:

1
2
3
4
5
6
7
use std::marker::PhantomData;

struct Slice<'a, T: 'a> {
start: *const T,
end: *const T,
phantom: PhantomData<&'a T>,
}

这反过来要求 T 类型中的任何引用在生命周期 'a 内都是有效的,初始化 Slice 时,仅需要为 phantom 字段提供值 PhantomData 即可:

1
2
3
4
5
6
7
8
fn borrow_vec<T>(vec: &Vec<T>) -> Slice<'_, T> {
let ptr = vec.as_ptr();
Slice {
start: ptr,
end: unsafe { ptr.add(vec.len()) },
phantom: PhantomData,
}
}
阅读全文 »

很多时候,我们需要实现一些自动优化的数据结构,在某些情况下是一种优化的数据结构和相应的算法,在其他情况下使用通用的结构和通用的算法。比如当一个 HashSet 的内容比较少的时候,可以用数组实现,但内容逐渐增多,再转换成用哈希表实现。如果我们想让使用者不用关心这些实现的细节,使用同样的接口就能享受到更好的性能,那么,就可以考虑用智能指针来统一它的行为。

我们来实现一个智能 StringRustString 在栈上占了 24 个字节,然后在堆上存放字符串实际的内容,对于一些比较短的字符串,这很浪费内存。

参考 Cow,我们可以用一个 enum 来处理:当字符串小于 N 字节时,我们直接用栈上的数组,否则使用 String。但是这个 N 不宜太大,否则当使用 String 时,会比目前的版本浪费内存。

当使用 enum 时,额外的 tag + 为了对齐而使用的 padding 会占用一些内存。因为 String 结构是 8 字节对齐的,我们的 enum 最小 8 + 24 = 32 个字节。

所以,可以设计一个数据结构,内部用1个字节表示字符串的长度,用 30 个字节表示字符串内容,再加上 1 个字节的 tag,正好也是 32 字节,可以和 String 放在一个 enum 里使用,我们暂且称这个 enumSmartString,它的结构如下图所示:

阅读全文 »

使用 std::sync::Mutex 可以多线程共享可变数据,MutexRwLock 和原子类型,即使声明为 non-mut,这些类型也可以修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

fn main() {
// 用 Arc 来提供并发环境下的共享所有权(使用引用计数)
let metrics: Arc<Mutex<HashMap<Cow<'static, str>, usize>>> =
Arc::new(Mutex::new(HashMap::new()));
for _ in 0..32 {
let m = metrics.clone();
thread::spawn(move || {
let mut g = m.lock().unwrap();

// 此时只有拿到 MutexGuard 的线程可以访问 HashMap
let data = &mut *g;

// Cow 实现了很多数据结构的 From trait,
// 所以我们可以用 "hello".into() 生成 Cow
let value = data.entry("hello".into()).or_insert(0);
*value += 1;

// MutexGuard 被 Drop,锁被释放
});
}

thread::sleep(Duration::from_millis(100));
println!("metrics: {:?}", metrics.lock().unwrap());
}
0%