Programming Rust 笔记

这本书已经是四年前的产物了。但是因为有中文翻译,所以我觉得看它会理解的更快一点
Rust 是一门系统编程语言。
系统编程是一种资源受限的编程。这种编程需要对每个字节和每个 CPU 时钟周期精打细算
Rust 实现上述所有承诺的关键在于所有权(ownership)、转移(move)和借用(borrow)机制造就的新型系统,而编译时检查和认真的设计又成就了 Rust 灵活的静态类型系统
- 所有权机制为每个值规划了清晰的生命期,从而让核心语言不再需要垃圾收集,同时还为管理套接口(socket)和文件勾柄(handle)等资源提供了可靠而又灵活的接口
- 转移把值从一个所有者转移给另一个所有者,
- 借用让代码可以临时使用某个值,同时又不影响其所有权
同样的所有权规则也是 Rust 值得依赖的并发模型的基础。说到互斥量(mutex)与其所要保护数据的关系,大多数语言是靠注释解决问题的。而 Rust 通过编译时检查可以发现访问被锁住的互斥量的问题。大多数语言只会告诫开发者要确保不访问已经交给其他线程的数据,Rust 却能通过检查保证你没有那么做。Rust 能够在编译时防止数据争用
如果将一个程序写得不可能在执行时导致未定义行为,那么就称这个程序为定义良好的(well defined)。如果一种语言的安全检查可以保证所有程序都定义良好,那么就称这种语言是类型安全的。
如果足够用心,用 C 或 C++ 应该也能写出定义良好的程序,但 C 和 C++ 不是类型安全的:前面的程序中没有类型错误,但出现了未定义行为。相对而言,Python 是类型安全的。Python 乐意花处理器时间来检查和处理数组索引越界的操作,方式也比 C 更友好
在 Python 中,ctype 是一个对 C 语言数据类型的封装。它允许 Python 代码访问和操作 C 语言中的数据类型,包括整数、浮点数、指针等。使用 ctype,Python 代码可以与 C 代码进行交互,并且可以使用 C 语言编写的库。这使得 Python 可以使用底层的 C 语言功能,提高程序的性能和效率。
rust 必须使用原始指针的情形。这种情况下你写的是不安全代码
fn gcd(mut n: u64, mut m: u64) -> u64 {
assert!(n != 0 && m != 0);
while m != 0 {
if m < n {
let t = m;
m = n;
n = t;
}
m = m % n;
}
n
}Rust 的机器整数类型名反映了它们的大小及有无符号,比如 i32 表示有符号 32 位整数,u8 表示无符号 8 位整数(用于“字节”值)。isize 和 usize 分别表示指针大小的有符号和无符号整数,在 32 位平台是 32 位,在 64 位平台则为 64 位。Rust 还有两个浮点类型 f32 和 f64,分别是 IEEE 单精度和双精度浮点类型,类似 C 和 C++ 的 float 和 double。
4 个空格的缩进是 Rust 风格
默认情况下,一个变量被初始化之后,它的值就不能变了。但是,像前面那样在参数 n 和 m 前加上 mut(发音为 [mjut],是 mutable 的简写)关键字,则表示可以在函数体内给它们赋值
! 符号表示这是一个宏调用,不是函数调用。与 C 和 C++ 中的 assert 宏类似,Rust 中的 assert! 宏会检查自己的参数是不是 true,如果不是,则终止程序并输出相关信息,包含检查失败的代码在源代码中的位置。这种突然的终止在 Rust 中叫诧异(panic)。与 C 和 C++ 中的断言可以跳过不同,Rust 不管程序是如何编译的都会检查断言。不过也有一个 debug_assert! 宏,它会在程序为速度而编译时被跳过
Rust 只在函数体内推断变量类型,函数参数和返回值则必须明确写出类型
如果函数体中最后一行代码是一个表达式,且表达式末尾没有分号,那这个表达式的值就是函数的返回值。用花括号括起来的任何代码块都可以看作一个表达式
这种在控制流“离开函数末尾”时生成函数值的方式是 Rust 特有的。return 语句一般只用于在函数的中间提前返回。
感觉也不是很特别……
#[test]
fn test_gcd() {
assert_eq!(gcd(14, 15), 1);
assert_eq!(gcd(2 * 3 * 5 * 11 * 17,
3 * 7 * 11 * 13 * 19),
3 * 11);
}#[test] 表示 test_gcd 是一个测试函数,在常规编译中会被跳过,但在通过 cargo test 命令运行程序时会包含并自动调用
#[test] 标记是属性(attribute)的一个例子。属性是一种开放式标记机制,用于给函数或其他声明添加补充说明,就像 C++ 和 C# 中的属性和 Java 中的注解一样。属性可以用于控制编译器报警和代码风格检查、有条件地包含代码(比如 C 和 C++ 中的 #ifdef)、告诉 Rust 如何与其他语言的代码互操作
use std::io::Write;
use std::str::FromStr;
fn main() {
let mut numbers = Vec::new();
for arg in std::env::args().skip(1) {
numbers.push(u64::from_str(&arg)
.expect("error parsing argument"));
}
if numbers.len() == 0 {
writeln!(std::io::stderr(), "Usage: gcd NUMBER ...").unwrap();
std::process::exit(1);
}
let mut d = numbers[0];
for m in &numbers[1..] {
d = gcd(d, *m);
}
println!("The greatest common divisor of {:?} is {}",
numbers, d);
}任何实现 Write 特型的类型都有用于将格式化文本写入输出流的 write_fmt 方法。std::io::Stderr 类型实现了 Write,而我们要使用 writeln! 宏来打印错误消息,这个宏展开之后的代码要使用 write_fmt 方法
任何实现 FromStr 特型的类型都有用于从字符串解析出该类型值的 from_str 方法。u64 类型实现了 FromStr,而我们会调用 u64::from_str 来解析命令行参数
尽管向量是可扩展和收缩的,但要向向量末尾推入数值,Rust 还要求必须给变量加上 mut 标记
std::env::args 函数返回一个迭代器(iterator),而迭代器会按需生成每一个参数,并在没有更多参数时指出来。迭代器在 Rust 中极其常用,标准库中也包含多种迭代器,比如迭代向量元素的、迭代文件的每一行的、迭代收到的每条消息的,总之,可以通过循环处理的任何数据都有对应的迭代器。Rust 的迭代器效率很高,编译器通常可以把它们转换成跟手写循环一样的代码
除了与 for 循环配合使用,迭代器本身也提供了很多能直接使用的方法。比如,std::env::args 返回的迭代器的第一个值始终是当前运行的程序的名字。我们想跳过这个值,因此可以调用迭代器的 skip 方法,从而生成一个不包含第一个值的新迭代器
u64::from_str u64 类型的方法,跟 C++ 或 Java 中的静态方法类似。而且 from_str 函数也不直接返回一个 u64 值,而是返回一个表示解析成功或失败的 Result 类型的值。Result 类型的值有两种:
- Ok(v) 表示解析成功,v 为得到的值;
- Err(e) 表示解析失败,e 为包含错误信息的值
所有涉及输入、输出或其他与操作系统交互的函数,都会返回 Result 类型的值。如果返回的是 Ok 值,则其中会包含操作成功的结果,可能是传输的字节数,也可能是打开的文件,等等;如果返回的是 Err 值,则其中会包含系统返回的错误码
Rust 没有异常的概念:所有错误都使用 Result 或诧异来处理
使用 Result 的 expect 方法检查解析的结果。如果结果是某种 Err(e),expect 就会输出 e 中包含的错误消息并立即退出程序。而如果结果是 Ok(v),expect 则直接返回 v
调用 writeln! 宏向标准错误输出流(由 std::io::stderr() 返回)中写入错误消息。最后调用 .unwrap() 是检查输出错误消息这个操作本身没有失败的一种快捷方式;当然调用 expect 也可以检查,只是没必要那么小题大做
现在要迭代一个向量,其大小并不确定,有可能会非常大。Rust 对待这种值非常慎重,它希望程序员来控制内存用度,明确指出每个值存活多久,同时还要保证不需要时立即释放内存
为此在迭代向量时要告诉 Rust,向量的所有权仍然属于 numbers,循环中仅仅是借用其元素而已。&numbers[1..] 中的 & 操作符表示从向量的第二个元素开始,借用每个元素的引用。for 循环迭代的是每个元素的引用,进而让 m 再去借用每个元素。*m 中的 * 操作符表示对 m 解引用,即取得引用所指的值,也就是要传给 gcd 函数的第二个 u64 值。最终,因为还是 numbers 拥有向量,所以 Rust 会在 numbers 脱离 main 函数末尾的函数作用域时自动将向量释放
Rust 认为只要 main 函数返回了,程序就成功结束了。只有明确调用 expect 或 std::process::exit 才会导致程序以某个错误状态码终止
extern crate iron;
#[macro_use] extern crate mime;
use iron::prelude::*;
use iron::status;
fn main() {
println!("Serving on http://localhost:3000...");
Iron::new(get_form).http("localhost:3000").unwrap();
}
fn get_form(_request: &mut Request) -> IronResult<Response> {
let mut response = Response::new();
response.set_mut(status::Ok);
response.set_mut(mime!(Text/Html; Charset=Utf8));
response.set_mut(r#"
<title>GCD Calculator</title>
<form action="/gcd" method="post">
<input type="text" name="n"/>
<input type="text" name="n"/>
<button type="submit">Compute GCD</button>
</form>
"#);
Ok(response)
}extern crate 指令,分别将 Cargo.toml 文件中指定的 iron 和 mime 引入程序中。extern crate mime 这一行前头的 #[macro_use] 属性会提醒 Rust,我们打算使用这个包导出的宏
按照约定,如果模块的名字叫 prelude,那就说明它所导出的特性是使用该包的任何用户都可能要用到的一些通用特性。因此这里在 use 指令中使用通配符 * 更合理一些
_request 变量前面加下划线的作用是告诉编译器这个变量是未使用的。在 Rust 语言中,如果定义了一个变量但没有使用它,会触发编译器警告。为了避免这个警告,可以在变量名前面加上下划线,表示这个变量是有意为之不使用的。这样做可以提醒其他开发者或代码审查者,表明这个变量是故意未使用的,并且不会对程序逻辑产生影响。
这段代码中,虽然函数 get_form 的参数_request 没有在函数体内被使用,但是它是为了满足 Iron 框架的要求而定义的
根据 Iron 框架的要求,处理请求的函数必须具有以下函数签名:
fn(&mut Request) -> IronResult<Response>
考虑到响应文本包含很多双引号,这里使用了 Rust 的“原始字符串”语法,即在 r(raw)后跟一个或多个 # 和一个双引号,然后是字符串内容,最后以另一个双引号及相同个数的# 结尾。原始字符串中的任何字符都无须转义,包括双引号。事实上,原始字符串中出现的以 \ 开头的转义序列(如 ")也不会被认为是转义序列。为避免歧义,通过在双引号两侧添加更多的 #,总是可以明确标识字符串的结束位置
extern crate urlencoded;
use std::str::FromStr;
use urlencoded::UrlEncodedBody;
fn post_gcd(request: &mut Request) -> IronResult<Response> {
let mut response = Response::new();
let form_data = match request.get_ref::<UrlEncodedBody>() {
Err(e) => {
response.set_mut(status::BadRequest);
response.set_mut(format!("Error parsing form data: {:?}\n", e));
return Ok(response);
}
Ok(map) => map
};
let unparsed_numbers = match form_data.get("n") {
None => {
response.set_mut(status::BadRequest);
response.set_mut(format!("form data has no 'n' parameter\n"));
return Ok(response);
}
Some(nums) => nums
};
let mut numbers = Vec::new();
for unparsed in unparsed_numbers {
match u64::from_str(&unparsed) {
Err(_) => {
response.set_mut(status::BadRequest);
response.set_mut(
format!("Value for 'n' parameter not a number: {:?}\n",
unparsed));
return Ok(response);
}
Ok(n) => { numbers.push(n); }
}
}
let mut d = numbers[0];
for m in &numbers[1..] {
d = gcd(d, *m);
}
response.set_mut(status::Ok);
response.set_mut(mime!(Text/Html; Charset=Utf8));
response.set_mut(
format!("The greatest common divisor of the numbers {:?} is <b>{}</b>\n",
numbers, d));
Ok(response)
}对某个 Result 值 res 而言,可以使用像下面这样的 match 表达式来检查它是哪种结果,并访问其中的值:
match res {
Ok(success) => { ... },
Err(error) => { ... }
}使用 match 表达式,程序必须先检查 Result 是哪种变量,然后才能访问它的值。因此我们永远不会错误地把一个失败的值当作成功的值。这正是 match 表达式精妙的地方。在 C 和 C++ 中,忘记检查错误码或空指针是一个常见的错误。而在 Rust 中,这类错误会在编译时被捕获到。这个简单的手段大幅提升了程序的可用性(usability)。
Rust 支持以包含值的变体创建类似 Result 的自定义类型,然后使用 match 表达式来对其分解求值。Rust 称这种自定义类型为枚举(enum),有的语言可能会称其为代数数据类型(algebraic data type)
就是说 result 类型像一个 enum 一样
<UrlEncodedBody> 是一种类型参数(type parameter),表示要取得 Request get_ref 的哪一部分。在此,UrlEncodedBody 类型指的是作为 URL 编码的查询字符串解析的请求主体
在使用互斥量协调多线程修改共享数据结构的情况下,Rust 可以保证只有持有锁的线程才能存取数据,而且操作完成后会自动释放锁。在 C 和 C++ 中,互斥量与它所保护数据的这种关系只能靠注释来描述
绘制曼德布洛特集合属于所谓的“尴尬并行”(embarrassingly parallel)算法,因为线程间的通信模式简单到让人尴尬。
无穷循环,使用了 Rust 专门为此编写的语法——loop 语句
extern crate num;
use num::Complex;
#[allow(dead_code)]
fn complex_square_add_loop(c: Complex<f64>) {
let mut z = Complex { re: 0.0, im: 0.0 }; loop {
z = z * z + c; }
}
struct Complex<T> {
re: T,
im: T
}
Complex 还是一个泛型(generic)结构体,代码中跟在类型名后面的 <T> 可以理解为“任 何类型 T”
extern crate num; use num::Complex;
fn escape_time(c: Complex<f64>, limit: u32) -> Option<u32> {
let mut z = Complex { re: 0.0, im: 0.0 }; for i in 0..limit {
z = z * z + c;
if z.norm_sqr() > 4.0 {
return Some(i); }
}
None }函数的返回值是一个 Option<u32>。Rust 标准库是这样定义 Option 类型的:
enum Option<T> {
None,
Some(T),
}z.norm_sqr() 方法调用返回 z 到原点距离的平方。为确定 z 是否离开了半径为 2 的圆,不计算平方根,而用距离的平方与 4.0 比较会更快。
use std::str::FromStr;
/// 解析字符串 s,格式为一对坐标值,如"400x600"或"1.0,0.5" ///
/// 特别地,s的格式应该是"<左值><分隔符><右值>"的形式,其中<分隔符>
/// 就是 separator 参数传入的字符,而<左值>和<右值>都是字符串,可以通过
/// T::from_str 来解析
///
/// 如果 s 的格式没错,就返回 Some<(x, y)>。如果解析出错,则返回 None
fn parse_pair<T: FromStr>(s: &str, separator: char) -> Option<(T, T)> {
match s.find(separator) { None => None,
Some(index) => {
match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) {
(Ok(l), Ok(r)) => Some((l, r)),
_ => None }
} }
}
#[test]
fn test_parse_pair() {
assert_eq!(parse_pair::<i32>("", ','), None);
assert_eq!(parse_pair::<i32>("10,", ','), None);
assert_eq!(parse_pair::<i32>(",10", ','), None);
assert_eq!(parse_pair::<i32>("10,20", ','), Some((10, 20)));
assert_eq!(parse_pair::<i32>("10,20xy", ','), None);
assert_eq!(parse_pair::<f64>("0.5x", 'x'), None);
assert_eq!(parse_pair::<f64>("0.5x1.5", 'x'), Some((0.5, 1.5)));
}(not completed)
Chapter 3
Rust 是静态类型的语言,即无须实际运行程序,编译器就可以检查所有可能的执行路径,确保程序以与类型一致的方式使用每一个值。
在 Python 或 JavaScript 中,所有函数天生可用于任意类型。只要给定的值拥有函数运行所需的属性和方法,那它就能使用这个函数。(这个特点通常被称为鸭子类型:如果叫起来像鸭子,走路也像鸭子,那它就是鸭子。)然而正是这种灵活性才导致这些语言不能提前检查出类型错误,要捕获这种错误只能靠测试。
Rust 的泛型函数为这门语言增添了一定程度的灵活性,同时还能保证在编译时捕获所有类型错误
与 C 和 C++ 不同,Rust 会将字符和数值类型区别对待,即 char 既不是 u8 也不是 i8。
尽管数值类型和 char 类型不一样,但 Rust 还是提供了字节字面量(byte literal),即用类字符字面量表示的 u8 值:b'X' 表示 ASCII 编码的字符 X,它是一个 u8 值。比如,由于字符 A 的 ASCII 编码是 65,因此 b'A' 等于 65u8。字节字面量中只能出现 ASCII 编码的字符
Rust 要求数组索引必须是 usize 值。另外,表示数组或向量的大小,或者某些数据结构中元素数量的值通常也是 usize 类型的
Rust 中的整数字面量可以通过一个后缀表示类型,比如 42u8 是一个 u8 值,而 1729isize 是一个 isize。如果整数字面量中不包含类型后缀,Rust 则会根据上下文推断其类型
使用 as 操作符实现整数类型之间的转换。
assert_eq!( 10_i8 as u16, 10_u16); // 在范围内Rust 中,i32 是一种基本的整数类型,它是 Rust 标准库(std)中的一部分
如果浮点字面量中没有类型后缀,那么 Rust 会根据上下文推断它是 f32 还是 f64,如果两种都有可能,则默认为 f64。
标准库的 std::f32 和 std::f64 模块为 IEEE 要求的特殊值定义了常量,比如 INFINITY、NEG_INFINITY(负无穷)、NAN(Not A Number,非数值),以及 MIN 和 MAX(最小和最大有限值)。模块 std::f32::consts 和 std::f64::consts 提供了各种常用的数学常量,比如 E、PI,以及 2 的平方根等
如果根据上下文推断不出来,可能就会出现令人不解的错误。比如,下面的代码就无法编译:
println!("{}", (2.0).sqrt());
Rust 会报错: error: no method named sqrt found for type {float} in the current scope
这有点不太好理解,不在浮点类型上找,难道还到其他类型上去找 sqrt 方法吗?解决方案是写出你想要的类型,以下两种写法都行:
println!("{}", (2.0_f64).sqrt());
println!("{}", f64::sqrt(2.0));总之,rust 不帮你隐式类型转换。表现为不能 if x,必须 if x != 0
虽然 bool 只需要一位即可表示,但 Rust 在内存里会使用整整一个字节作为 bool 值,因此可以创建一个指向它的指针
Rust 使用 char 类型表示单个字符,但对字符串或文本流使用 UTF-8 编码。因此,String 将其文本表示为一个 UTF-8 字节的序列,而不是字符的数组
元组的每个元素都可以是一个不同的类型,而数组的元素必须都是同一类型。其次,元组只允许用常量作为索引
Rust 代码中的函数经常通过元组来返回多个值。比如,字符串切片的 split_at 方法会将一个字符串切成两半,然后返回两个字符串
fn write_image(filename: &str, pixels: &[u8], bounds: (usize, usize))
-> Result<(), std::io::Error>
{ ... }bounds 参数的类型为 (usize, usize),即两个 usize 值的元组。没错,这里直接传一个 width 再传一个 height 也可以,最终的机器码可能也差不多。但这是一个怎么写更清晰的问题。我们认为大小是一个值而不是两个,元组恰好可以表达这种意图
另一个常用的元组类型可能会让人难以置信,它就是零元组 ()。这种元组过去一直被称为基元类型(unit type),因为它只有一个值,也写作 ()。当不存在有意义的值而上下文又要求某种类型时,Rust 会使用这种基元类型
std::mem::swap 的声明如下: fn swap<T>(x: &mut T, y: &mut T);
Rust 允许在函数参数、数组、结构体、枚举等任何使用逗号的地方的末尾再加一个逗号。这在人类看来可能有点怪,但在列表末尾发生了元素的增删操作之后,差异会比较容易看出来。
出于一致性的考虑,甚至可以有包含一个单一值的元组。字面量 ("lonely hearts",) 就是只包含一个单一字符串的元组,其类型为 (&str,)。这时候,末尾的逗号就是必需的了,没有它则无法区分这是个元组还是简单的括号表达式。
Java 中,如果 class Tree 包含一个字段 Tree left;,那么 left 就是一个指向在其他地方创建的另一个 Tree 对象的引用。Java 中的对象从来不会在物理上包含另一个对象。Rust 不同,其设计目标就是保持内存占用最小化,因此 Rust 中的值默认是嵌套的,比如 ((0, 0), (1440, 900)) 会被保存为 4 个相邻的整数。如果把它保存在一个局部变量中,那它就变成了一个 4 个整数宽的局部变量。此时不会占用堆内存。
Java 中,对象通常在堆内存中进行分配和管理。Java 中的对象是通过使用 new 关键字来动态地在堆上分配内存空间创建的。
当我们在 Java 中使用 new 关键字创建一个对象时,它会在堆内存中为该对象分配足够的空间来存储对象的实例变量和其他相关信息。对象在堆上分配的内存空间通常由 Java 的垃圾回收器进行管理,即在对象不再被引用时,垃圾回收器可以自动回收该对象所占用的内存空间。
堆内存的好处是可以提供动态分配和释放内存的灵活性,允许对象的生命周期在运行时动态改变。然而,由于需要动态分配内存并进行垃圾回收,堆内存的管理可能会引入一些额外的开销和复杂性。
除了堆内存之外,Java 还有栈内存用于存储局部变量和方法调用等。栈内存的分配和释放是自动完成的,不需要手动管理,且速度较快。而堆内存的分配和释放需要更复杂的垃圾回收机制来管理。
这对提升内存使用效率非常重要,但同时也带来了一个后果:当 Rust 程序中的值需要指向其他值时,就必须显式地使用指针类型
尽管 Java 中没有像 Rust 中那样的显式引用类型,但在 Java 中操作对象时,我们实际上是在使用引用。Java 中的引用机制提供了方便的自动内存管理,使开发者无需手动分配和释放内存,同时确保内存安全性和避免悬垂指针等问题。
与 C 指针不同的是,Rust 引用永远不会是空值,因为在安全 Rust 代码中根本不可能产生空引用。而且 Rust 引用默认是不可修改的
在堆上分配一个值的最简单方式就是使用 Box::new:
let t = (12, "eggs");
let b = Box::new(t); // 在堆中分配一个元组t 的类型是 (i32, &str),因此 b 的类型是 Box<(i32, &str)>。Box::new() 会为这个元组在堆上分配足够大的内存。而当 b 超出作用域之后,内存会自动释放,除非 b 被转移了(比如被返回)。
转移是 Rust 处理分配到堆上的值的根本方式
- 类型 [T; N] 表示一个 N 个值的数组,每个值的类型都是 T。数组的大小是在编译时确定的常量,也是类型自身的一部分。换句话说,不能向数组中追加新元素,也不能缩短数组
- 类型
Vec<T>叫作 T 类型的向量,是一种动态分配、可扩展的类型 T 的值序列。向量的元素保存在堆上,因此可以随意缩放它 - 类型 &[T] 和 &mut [T] 叫作 T 类型的共享切片和 T 类型的可修改切片,是对其他值(比如数组或向量)中部分元素序列的引用。可以把切片想象成一个指针,包含切片中第一个元素的地址和从这个元素起可以访问元素的数量。可修改切片&mut [T]允许读写元素,但不能共享;而共享切片 &[T] 可以由多个读者读取,但不允许修改元素
还有一种写法,即 [0u8; 1024],用于表示固定大小的缓冲区(这里是 1 KB 的缓冲区,以零字节填充)。Rust 没有语法表示未初始化的数组。(通常,Rust 会保证代码永远不访问任何未经初始化的值。)
数组的长度是其类型的一部分,在编译时就确定了
我们看到的用于数组的方法,比如迭代元素、搜索、排序、填充、筛选等,都是切片的方法,并非数组的方法。不过,Rust 会在查找方法时隐式地把对数组的引用转换为对切片的引用,因此可以直接在数组上调用切片的任何方法
创建向量的方式有好几种。最简单的方式是使用 vec! 宏,调用 vec! 宏等价于调用 Vec::new 创建一个新的空向量,然后再向其中推入元素
另一种方式是基于迭代器产生的值来构建向量:
let v: Vec<i32> = (0..5).collect();
assert_eq!(v, [0, 1, 2, 3, 4]);在使用 collect 时通常要写明类型(就像这里一样),因为这个方法可以构建多种集合,不仅是向量
一个 Vec<T> 包含 3 个值:对分配在堆上用于保存元素的缓冲区的引用、该缓冲区可以存储的元素个数(也就是容量),以及当前实际存储的元素个数(也就是长度)。当缓冲区达到其容量上限后,再给向量添加元素会导致一系列操作:分配一个更大的缓冲区,将现有内容复制过去,基于新缓冲区更新向量的指针和容量,最后释放旧缓冲区
如果提前知道向量中需要保存的元素个数,可以使用 Vec::with_capacity(而不是 Vec::new)先创建一个有足够大缓冲区存储所有元素的向量,然后再逐个向向量中添加元素,从而避免内存的重新分配。vec! 宏使用了与此类似的一个技巧,因为它知道向量最终会拥有多少元素。
很多库函数会尽量使用 Vec::with_capacity,避免使用 Vec::new。
使用 pop 方法可以删除并返回最后一个元素。更准确地说,是从 Vec<T> 中删除一个值,返回一个 Option<T>:如果向量是空的则返回 None;如果最后一个元素是 v 则返回 Some(v):
3.4.3 没怎么懂
切片(slice),写作不指定长度的 [T],表示数组或向量的一个范围。由于切片可以是任意长度,因此不能直接保存到变量中,也不能作为函数参数传递。切片永远只能按引用传递
切片的引用是一个胖指针(fat pointer),即一个双字宽的值,保存着指向切片第一个元素的指针和切片中元素的个数
普通引用是对单个值的非所有型指针,切片引用则是对多个值的非所有型指针。因此切片引用非常适合操作一串同类数据的函数,无论这串数据存储在数组、向量,抑或栈还是堆上
fn print(n: &[f64]) {
for elt in n {
println!("{}", elt);
}
}我们经常把 &[T] 或 &str 这样的引用类型称为切片,其实是有点偷懒了。实际上,应该称它们为对切片的引用。不过因为提到切片基本上都是指对它的引用,所以也就用“切片”来指代“切片引用”了
熟悉 C++ 的程序员会记得该语言中有两种字符串类型。字符串字面量拥有指针类型 const char *。标准库也定义了一个类,即 std::string,用于在运行时动态创建字符串。Rust 也采用了类似的设计
有时候,把所有反斜杠都打两遍也很烦人(经典的例子就是正则表达式和 Windows 路径)。针对这种情况,Rust 提供了原始字符串(raw string)语法。原始字符串以小写字母 r 为标记,其中的所有反斜杠和空白符都会原样包含在字符串中,转义序列无效。
要在原始字符串中包含双引号,简单地在双引号前面加个反斜杠是不行的。前面刚说过,原始字符串中的转义序列无效。不过也有办法,即在原始字符串的开头和末尾添加井字号标记:
println!(r###"
This raw string started with 'r###"'.
Therefore it does not end until we reach a quote mark ('"')
followed immediately by three pound signs ('###'):
"###);Rust 字符串就是 Unicode 字符序列,但它在内存中不是以 char 数组的形式存储的。事实上,字符串在内存中使用 UTF-8 可变宽度编码存储。字符串中的每个 ASCII 字符用 1 个字节存储,其他字符则占用多个字节。
String 有一个可伸缩缓冲区用于存储 UTF-8 文本。这个缓冲区分配在堆上,因此可以按需要或请求来调整大小。可以把 String 想象成一个能够存储格式完好的 UTF-8 的 Vec<u8>。事实上,String 也确实是这么实现的
&str(发音“stir”或叫“字符串切片”)是对其他变量拥有的一串 UTF-8 文本的引用:它“借用”了这些文本。&str 也是一个胖指针,包含实际数据的地址及其长度。可以把 &str 想象成就是一个 &[u8],只不过它能存储格式完好的 UTF-8
不要试图修改 &str,因为它是无法修改的.如果想在运行时创建新字符串,那么要使用 String。
确实存在 &mut str 类型,但用处不大,因为几乎对 UTF-8 的任何操作都会改变其总字节长度,而切片又不可能重新分配它引用的缓冲区。事实上,&mut str 上仅有两个方法,即 make_ascii_uppercase 和 make_ascii_lowercase,根据定义它们只就地修改文本且只影响单字节字符
&str 类似 &[T],就是一个指向某些数据的胖指针。String 则类似 Vec<T>
创建 String
- .to_string() 方法可以把 &str 转换为 String。实际上是复制字符串
- format!() 宏跟 println!() 类似,区别在于它返回一个新 String 而不是将文本写入标准输出,并且不会自动在末尾加换行符
- 字符串的数组、切片和向量有两个方法:.concat() 和 .join(sep),可以将多个字符串拼接成一个新 String
当调用者可以使用任意一种字符串时,&str 更适合作为函数参数。因为 &str 可以引用任何字符串的任意切片即可,无论它是(存储在可执行文件中的)字符串字面量,还是(运行时分配和释放的)String
字符串支持 == 和 != 操作符。如果两个字符串包含的字符相同、顺序也相同,那它们就是相等的,无论它们是否指向内存中的同一个地址
但要记住,Unicode 的特性决定了简单的逐个字符比较不一定得到想要的结果。比如,Rust 字符串 "th\u{e9}" 和 "the\u{301}" 都是 thé(法语“茶”)的有效 Unicode 表现形式。对这两个字符串,Unicode 认为无论显示和处理都应该一视同仁,但 Rust 认为它们是两个完全不同的字符串。类似地,像 < 这样的比较操作符只会简单地基于字符码点值的词典顺序进行比较
Rust 的解决方案是提供几个类似字符串的类型。
- 对于 Unicode 文本,使用 String 和 &str。
- 处理文件名时,使用 std::path::PathBuf 和 &Path。
- 处理根本不是字符数据的二进制数据时,使用
Vec<u8>和 &[u8]。 - 处理以操作系统原生形式表示的环境变量名和命令行参数时,使用 OsString 和 &OsStr。
- 与使用空字符结尾字符串的 C 库互操作时,使用 std::ffi::CString 和 &CStr。
Chapter 4
如果你读过比较多的 C 或 C++ 代码,那可能看到过有注释解释说某个类的实例拥有它所指向的一些其他对象。这种情况通常意味着拥有对象来决定什么时候释放它所有的对象,即在所有者被销毁的时候,它也会销毁与之相关的资源