前言
笔记的内容主要参考与《Rust 程序设计语言》,一些也参考了《通过例子学 Rust》和《Rust语言圣经》。
Rust学习笔记分为上中下,其它两个地址在Rust学习笔记(中)和Rust学习笔记(下)。
编译与运行
Rustup(updater)
它是一个管理 Rust 版本和相关工具的命令行工具。
Rustc(compiler)
Rust 是一种预编译静态类型(ahead-of-time compiled)语言,这意味着你可以将编译程序生成的可执行文件送给其他人,他们甚至不需要安装 Rust 就可以运行。而像其它动态语言(Ruby、Python 或 JavaScript)则做不到,需要分别安装对应的环境,这一切都是语言设计上的权衡取舍。
shell
# 编译成功后,Rust会输出一个二进制的可执行文件(main)。
$ rustc main.rs
# 运行
$ ./main
Cargo(package manager)
仅仅使用 rustc
编译简单程序是没问题的,不过随着项目的增长,你可能需要管理你项目的方方面面,并让代码易于分享。这需要用到Cargo。Cargo 是 Rust 的构建系统和包管理器。
shell
$ cargo new hello_cargo
$ cd hello_cargo
\hello_cargo
|-- src
|-- main.rs
|-- Cargo.toml
对于 cargo new
命令,它会自动添加 Git 进行版本管理,如不想使用,可以添加 -- vcs
(version control system)进行改变,例如 cargo new hello_cargo --vcs none
在 Cargo.toml 文件中,使用 TOML(Tom's Obvious, Minimal Language)格式编写。第一行,[package]
,是一个片段(section)标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他片段(section)。在 [package]
中,包含了项目的名称、版本、作者以及要使用的 Rust 版本。最后一行,[dependencies]
,是用来罗列项目依赖,在 Rust 中,代码包被称为 crates 。也可通过命令的方式安装 cargo add xxx
,它会默认从 Crates.io(提供 Rust 的各种依赖)安装最新版。也可使用 cargo update
来更更新依赖的版本。
toml
[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"
[dependencies]
Cargo 生成项目的区别是 Cargo 将代码放在 src 目录,同时项目根目录包含一个 Cargo.toml 配置文件。Cargo 期望源文件存放在 src 目录中。项目根目录只存放 README、license 信息、配置文件和其他跟代码无关的文件。
使用Cargo编译与运行用到了如下命令:
shell
# 这个命令会创建一个可执行文件target/debug/hello_cargo,可以通过./target/debug/hello_cargo运行
$ cargo build
# 更简单的运行方式,是使用下面的语句
$ cargo run
# 该命令快速检查代码确保其可以编译,但并不产生可执行文件
$ cargo check
当使用 cargo build
时,会在项目根目录创建一个新文件:Cargo.lock, 这个文件记录项目依赖精确版本信息。
通常 cargo check
要比 cargo build
快得多,因为它省略了生成可执行文件的步骤。如果你在编写代码时持续的进行检查,cargo check
会加速开发。
最后,当项目最终准备好发布时,可以使用 cargo build --release
来优化编译项目。这会在 target/release 而不是 target/debug 下生成可执行文件。这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。
为什么需要 Cargo.lock
Cargo.lock 可以确保构建是可重现的,在第一次运行 cargo build
时创建,Cargo 会计算出所有符合要求的依赖版本(包含间接依赖文件)写入该文件。这样在大型项目中,能够锁定整个项目依赖树的精确版本,这包括所有的直接和间接依赖,因为可能有多个依赖项共享相同的间接依赖,或不同的间接依赖。总之就是避免冲突,保证重现。
基本语法
变量与常量(Variable and constant)
rust
// 直接使用let声明,类型可以不写,Rust会自动识别。对于整型这种会取为默认i32,只有明显大于或小于i32范围才会换为别的
let word = "abc";
let price: i32 = 199;
// mut:mutable,声明变量是可变的,否则变量无法更改
let mut checked = true;
// 常量声明(这里的下划线可有可无,只是提升可读性)
const MAX_POINTS: u32 = 100_000;
// 覆盖原值
let word = "def"
不可变变量与常量的区别
- 常量不能使用
mut
。 - 常量必须标明类型。
- 常量可以在任何作用域中声明,包括全局作用域,变量不可以。
- 常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值(可以 1 + 1 ,不可以 1 + x )。
隐藏(Shadowing)
可以重新使用 let
声明存在的值,会将其覆盖。
数据类型(data type)
-
标量(scalar)
- 整型(integer):有符号整数类型以 i(signed)开头,无符号以 u(unsigned)开头。默认 i32。以有符号为例,分为了 i8(1字节)、i16(2字节)、i32(4字节)、i64(8字节)、i128(16字节)。1字节 = 8位,根据这个可算出其范围。还有一个类型为 isize,类型依赖运行程序的计算机架构,64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。
- 浮点型(floating-point numbers):Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。声明时要像这样
let x = 2.0;
。 - 布尔型(bool):true 和 false。
- 字符类型(character):为char,像
let c = 'z'
,大小为四个字节(four bytes),代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。
-
复合(compound)
-
元组(tuple):可以将多个其他类型的值组合进一个复合类型方式。元组长度固定,一旦声明,其长度不会增大或缩小。
rust// 也可不写类型 let tup: (i32, f64, u8) = (500, 6.4, 1); // 访问 // 方式1:使用模式匹配(pattern matching)来解构(destructure)元组值 let (x, y, z) = tup; println!("The value of y is: {}", y); // 方式2:点号加索引 println!("The value of x is: {}", tup.0);
对于元组还有另一个用处,接函数值。
rustfn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); (s, length) }
-
数组(array):可以包含多个相同类型的值,Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的,一旦声明,它们的长度不能增长或缩小。数组并不如 vector 类型灵活,vector 类型是标准库提供的一个允许增长和缩小长度的类似数组的集合类型。
rust// 声明 let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; // 也可以加上类型,和长度 let a: [i32; 5] = [1, 2, 3, 4, 5]; // 可以创多个重复值的数组(5个3) let a = [3; 5]; // 访问 let first = a[0];
-
整型溢出(integer overflow)
比方说有一个 u8,它可以存放从零到 255 的值。那么当你将其修改为 256 时会发生什么呢?
当在 debug 模式编译时,会使程序 panic;在 release 构建中,Rust 不检测溢出,256 会变成 0,257 变成 1,依此类推。
数值运算
不同类型的整数之间可以随意加减乘除(小心溢出),但对于浮点数,运算时每个值的类型都必须相同。
无效的数组元素访问
一般程序语言数组越界访问可能导致无效内存访问,这可能会引起程序崩溃或安全漏洞(如缓冲区溢出)。与之相比,Rust 在运行时执行边界检查,当尝试访问超出数组长度的元素时,程序会 panic 并安全退出。这种行为防止了潜在的无效内存访问,提高了程序的整体安全性。
函数
基本格式
rust
fn another_function(x: i32,y: i32) -> i32 {
...
}
语句(Statements)和表达式(Expressions)
rust
// 语句
let y = 6;
// 会报错,不能把let语句给x
let x = (let y = 6);
// { let x = 3; x + 1}是一个表达式
let y = {
let x = 3;
x + 1
};
// 表达式直接返回,不能加分号。如果想加可以用return;
fn plus_one(x: i32) -> i32 {
x + 1
}
两种函数返回值的方式
rust
fn plus_one(x: i32) -> i32 {
// 第一种
2
//第二种
return 2;
}
控制流
判断
rust
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
// 不能这样
let number = 3;
if number {
println!("number was three");
}
// let if的用法(和if let不一样)
let condition = true;
let number = if condition {
5
} else {
// 上下两个值类型需相同。Rust需要在编译时就确切的知道number变量的类型,这样它就可以在编译时验证在每处使用的number变量的类
// 型是否有效。Rust并不能够在number的类型只能在运行时确定的情况下工作,这样会使编译器变得更复杂而且只能为代码提供更少的保
// 障,因为它不得不记录所有变量的多种可能的类型。
6
};
循环
rust
// loop
loop {
println!("again!");
}
// 这里为什么可以?因为Rust的break不太一样,break counter * 2像是return counter * 2
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
// while
while number != 0 {
println!("{}!", number);
number = number - 1;
}
// for
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {}", element);
}
for number in (1..=4).rev() { // rev用来反转range
println!("{}!", number);
}
所有权(ownership)
Rust 的核心功能之一就是所有权。所有运行的程序都必须管理其使用计算机内存的方式,一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存;Rust 则选择了第三种方式,通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。
栈(Stack)和堆(Heap)
在很多语言中,你并不需要经常考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。
栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值,这也被称作后进先出(last in, first out)。栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针(pointer)。这个过程称作在堆上分配内存(allocating on the heap)。将数据推入栈中并不被认为是分配。
入栈比在堆上分配内存要快,因为(入栈时)操作系统无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
在访问时,堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快。对于栈,当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的存在就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。
函数与栈
我刚开始特别纠结,栈不是 LIFO 吗,那我先存入一个变量,在存入另一个,那我想用第一个怎么用?
这里栈的应用不是我想象的呢么简单,涉及汇编比较底层,简单解释一下,这里存入栈的是各种指令。像对于这种拿变量,都是直接通过什么指令给到对方地址,当函数结束,这些指令也会出栈。
所有权规则
- Rust 中的每一个值都有一个被称为其所有者(owner)的变量。
- 值有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
变量的作用域(scope)
在有垃圾回收(garbage collector*,*GC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们的责任了,跟请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要精确的为一个 allocate
配对一个 free
。Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。
rust
{ // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效
内存与分配(Memory and allocation)
-
变量与数据交互的方式(一):移动(move)
rust// 正确 let x = 5; let y = x; println!("{}", x) // 错误 let s1 = String::from("hello"); let s2 = s1; println!("{}", s1);
首先对于像整形这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的意味着没有理由在创建变量
y
后使x
无效。Rust 有一个叫做Copy
trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上。如果一个类型拥有Copy
trait,一个旧的变量在将其赋值给其他变量后仍然可用。而对于像 String 这样在堆上的值,Rust 不像别的语言可能有浅拷贝和深拷贝的说法,会直接让第一个值无效,这个操作被称为移动(move)。Rust 永远也不会自动创建数据的 "深拷贝"。因此,任何自动的复制可以被认为对运行时性能影响较小。
-
变量与数据交互的方式(二):克隆(clone)
如果确实需要深度复制 String 中堆上的数据,可以使用
clone
来操作。rustlet s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2);
所有权与函数
这里要注意的是像整形这种值,赋给函数后,还能用。
rust
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用drop方法,占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer移出作用域。不会有特殊操作
对于返回值,也同理。
rust
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 将返回值移动给
// 调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
引用与借用(reference and borrow)
如果我们想要函数使用一个值但不获取所有权该怎么办呢?可以使用 &
引用( &
引用相反的操作是 解引用 (dereferencing),它使用解引用运算符,*
)。我们将获取引用作为函数参数称为借用(borrowing)。
rust
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
// 所以什么也不会发生
&s1
语法让我们创建一个指向值 s1
的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
但是这个引用是不可变的,如想要更改原引用的值,可以使用&mut
rust
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。下面的代码会报错。这个限制的好处是 Rust 可以在编译时就避免数据竞争(data race)。它可由这三个行为造成:两个或更多指针同时访问同一数据;至少有一个指针被用来写入数据;没有同步数据访问的机制。数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
rust
// 报错
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2); // 不用r1就没事
// 报错
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题,这里怎么想呢,从语义来说,r1和r2认为s是不变的,结果变了
println!("{}, {}, and {}", r1, r2, r3);
// 上面这个如果变成下面这样是可以运行的,也就是说只要你之后不再用r1和r2就没问题
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
println!("{}", r3);
悬垂引用(Dangling References)
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
rust
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 解决方式,直接返回s
fn no_dangle() -> String {
let s = String::from("hello");
s
}
Slice 类型
用于提供对集合中一段连续元素的引用,也就是能保证和原集合能够同步变化。"字符串 slice" 的类型声明写作 &str
:
rust
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
// 更好的写法可能是下面的,因为它使得可以对 String 值和 &str 值使用相同的函数:
fn first_word(s: &String) -> &str {
fn first_word(s: &str) -> &str {
结构体
rust
// 声明
struct User {
// field
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
// 使用
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// 可变使用
// Rust并不允许只将某个字段标记为可变,要想变,整个实例都得可变
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
// 也可直接返回一个
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
// 使用没有命名字段的元组结构体来创建不同的类型
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
注意如果要在 Struct 里用字符串只能 String,不能 &str 。因为结构体拥有它的数据的所有权,如果用 &str ,可以来自于外部 String ,那么外部 String 失效,会导致 struct 失效。
rust
struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
一个使用结构体的示例程序
rust
// 计算长方形面积一步一步的重构
// 这两个参数是相关联的,不过程序本身却没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
// 使用元组重构,不过元组并没有给出元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
// 使用结构体,赋予更多意义
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
// 其他
// 通过派生 trait 增加实用功能
#[derive(Debug)] // 直接是无法打印struct的,必须有Debug这个trait
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 is {:?}", rect1);
// 另一种风格
println!("rect1 is {:#?}", rect1);
}
方法
fn 被称作函数,而 impl 被称作方法。与 fn 不同的是,impl 在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文),通过 impl 块将方法与特定的结构体(或枚举)关联起来,这意味着这些方法是与特定类型紧密相关的操作,而不是一些独立的、无关的函数,这有助于组织和模块化代码,使得代码更加易于理解和维护。
并且 Rust 虽然不是一种传统的面向对象语言,但它支持面向对象的一些核心概念,比如封装、继承(通过特质)和多态。使用 impl 块定义方法是实现封装和多态的一种方式。它允许类型定义它们自己的行为,以及通过特质为不同类型提供通用接口。
rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// 这里的名字需要和struct一样
impl Rectangle {
// &self来替代rectangle: &Rectangle,如果想要改变rectangle需要变成&mut self(很少见)
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("The area of the rectangle is {} square pixels.", rect1.area());
}
// 关联函数,正如名字,相关联的,放在一个impl
// 它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例
// 使用时需要"::",let sq = Rectangle::square(3);
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}
调用方法中 :: 和 . 的区别
就像上面提到的,和结构体本身没什么联系的用 ::
,有联系的用 .
。下面是一个其他例子,这里 ::
其实就和 Java 中的 new 一个作用,就是 new 出一个实例。
也有另一种情况,比如在 main 中引入另一个包,没有结构体,包里面全是函数,你要使用的话也得 ::
。
rust
struct Person {
name: String,
age: u8,
}
impl Person {
// 关联函数(静态方法),用于创建一个新的 Person 实例
fn new(name: &str, age: u8) -> Person {
Person {
name: name.to_string(),
age,
}
}
// 实例方法,调用时需要一个 Person 的实例
fn say_hello(&self) {
println!("Hello, my name is {} and I am {} years old.", self.name, self.age);
}
}
rust
fn main() {
// 调用关联函数(静态方法)来创建一个 Person 实例
let person = Person::new("Alice", 30);
// 调用实例方法
person.say_hello();
}
枚举
什么时候使用枚举呢?像下面的场景,任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的,而且不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。
rust
// 声明
enum IpAddrKind {
V4,
V6,
}
// 使用
let four = IpAddrKind::V4; // 这样写也暗示了V4和V6是属于IpAddrKind类型的
let six = IpAddrKind::V6;
// 对于函数
fn route(ip_type: IpAddrKind) { }
route(IpAddrKind::V4);
枚举还可以存放数据
rust
// 传统方式
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
// 枚举方式
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
// 更夸张的,枚举可以存放任何混合类型
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
枚举也可以使用 impl。
rust
impl Message {
fn call(&self) {
// 在这里定义方法体
}
}
let m = Message::Write(String::from("hello"));
m.call();
Option
Rust 并没有很多其他语言中有的空值功能,与之类似的是 Option
。Option<T>
枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 Option::
前缀来直接使用 Some
和 None
。即便如此 Option<T>
也仍是常规的枚举,Some(T)
和 None
仍是 Option<T>
的成员。
rust
enum Option<T> {
Some(T),
None,
}
如果使用 None
而不是 Some
,需要告诉 Rust Option<T>
是什么类型的,因为编译器只通过 None
值无法推断出 Some
成员保存的值的类型。对于 <T>
是一个泛型,任意类型的数据。
rust
// 例子
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
当有一个 Some
值时,我们就知道存在一个值,而这个值保存在 Some
中。当有个 None
值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么重点来了,Option<T>
为什么就比空值要好呢?
首先看下面的例子,是会报错的,因为Option<i8>
与 i8
类型不同,无法相加。当在 Rust 中拥有一个像 i8
这样类型的值时,编译器确保它总是有一个有效的值,我们可以自信使用而无需做空值检查;而当使用 Option<i8>
的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。换句话说,在对 Option<T>
进行 T
的运算之前必须将其转换为 T
。这意味着我们不得不处理可能为空的情况,这样也就避免了遇到为空的问题。
rust
// 报错
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
prelude
默认情况下,Rust 会自动将 prelude 引入,使得一些非常常用的类型和特质可以直接使用。如果需要的类型不在 prelude 中,你必须使用use
语句显式地将其引入作用域。另外常见的 std 为标准库,prelude 属于 std。
match
rust
// 常规用法
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
}
}
// 匹配Option
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
// 不像列举所有
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
注意匹配必须是穷尽的(exhaustive),也体现出上面提到过Option的意义,必须处理空值的情况
rust
// 报错
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
if let
rust
let some_u8_value = Some(0u8);
// 像这里我们其实只关心为Some(3)的情况,我们可以使用if let
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}
if let Some(3) = some_u8_value {
println!("three");
}
然而,这样会失去 match
强制要求的穷尽性检查。match
和 if let
之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
rust
// if let也可以加
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
这里区分一下之前出现的用法:
rust
if let 1 = 1 {
println!("1");
}
let a = if true { // 这里必须放bool值
1
} else {
2
};
项目管理
包(package)和 crate
包是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。
包中所包含的内容由几条规则来确立。一个包中至多只能包含一个库 crate(library crate);包中可以包含任意多个二进制 crate(binary crate);包中至少包含一个 crate,无论是库的还是二进制的。
(library crate:是提供一组特定功能的代码库,旨在被其它crate作为依赖项引用。它们不直接编译成可执行文件,而是被编译成rlib或dylib文件,供其它项目引用。通常用于定义函数、类型、trait等可以被其他项目重用的代码。库crate使得代码复用、模块化和分发变得简单。binary crate:是编译成单独的可执行文件的crate,它包含一个或多个main函数作为程序入口点。用于创建最终用户可以直接运行的应用程序。这些程序可以做任何事情,从命令行工具到服务器和客户端应用程序。)
当我们输入 Cargo new
,Cargo 会给我们的包创建一个 Cargo.toml 文件。查看 Cargo.toml 的内容,会发现并没有提到 src/main.rs,因为 Cargo 遵循的一个约定:src/main.rs 就是一个与包同名的二进制 crate 的 crate 根。同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。crate 根文件将由 Cargo 传递给 rustc
来实际构建库或者二进制项目。如果一个包同时含有 src/main.rs 和 src/lib.rs,则它有两个 crate:一个库和一个二进制项,且名字都与包相同。
额外说一下,一个crate,将将其功能保持在其自身的作用域中,可以知晓一些特定的功能是在我们的 crate 中定义的还是在 别的crate 中定义的,这可以防止潜在的冲突。
模块(Module)
模块让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。模块还可以控制项的私有性。
在 src/lib.rs,下面的内容就是一个包含了其他内置了函数的模块的 front_of_house
模块。
rust
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn server_order() {}
fn take_payment() {}
}
}
在前面我们提到了,src/main.rs
和 src/lib.rs
叫做 crate 根。之所以这样叫它们的原因是,这两个文件的内容都是一个从名为 crate
的模块作为根的 crate 模块结构,称为模块树(module tree)。
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
路径
路径用于引用模块树中的项,分为绝对路径(absolute path)从 crate 根开始,以 crate 名或者字面值 crate
开头;相对路径(relative path)从当前模块开始,以 self
、super
或当前模块的标识符开头。路径的递进由 ::
完成。如何调用 add_to_waitlist
函数?那就是用它的路径。
下面文件在 src/lib.rs 中, eat_at_restaurant
函数是我们 crate 库的一个公共API,所以我们使用 pub
关键字来标记它。这个模块在模块树中,与 eat_at_restaurant
定义在同一层级。
rust
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
什么时候用绝对路径什么时候相对路径呢?如果我们要将 front_of_house
模块和 eat_at_restaurant
函数一起移动到一个名为新的 customer_experience
的模块中,我们需要更新 add_to_waitlist
的绝对路径,但是相对路径还是可用的。然而,如果我们要将 eat_at_restaurant
函数单独移到一个名为新的 dining
的模块中,还是可以使用原本的绝对路径来调用 add_to_waitlist
,但是相对路径必须要更新。我们更倾向于使用绝对路径,因为它更适合移动代码定义和项调用的相互独立。
模块不仅对于你组织代码很有用。他们还定义了 Rust 的私有性边界(privacy boundary):这条界线不允许外部代码了解、调用和依赖被封装的实现细节。所以,如果你希望创建一个私有函数或结构体,你可以将其放入模块。
Rust 中默认所有项(函数、方法、结构体、枚举、模块和常量)都是私有的。父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文。
像上面的代码其实是错误的,如果想访问 hosting
以及 add_to_waitlist
需要为它添加 pub
字段。
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
还可以使用 super
开头来构建从父模块开始的相对路径。这么做类似于文件系统中以 ..
开头的语法。下面的例子也可体现出子访问父不需要 pub
。
rust
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
还可以使用 pub
来设计公有的结构体和枚举,不过有一些额外的细节需要注意。如果我们在一个结构体定义的前面使用了 pub
,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。我们可以根据情况决定每个字段是否公有。
rust
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
// eat_at_restaurant无法更改,所以必须在方法中做这步
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
}
与之相反,如果我们将枚举设为公有,则它的所有成员都将变为公有。给枚举的所有成员挨个添加 pub
是很令人恼火的,因此枚举成员默认就是公有的。结构体通常使用时,不必将它们的字段公有化,因此结构体遵循常规,内容全部是私有的,除非使用 pub
关键字。
rust
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
use
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// 相对路径也是可以的
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
为什么不用 use crate::front_of_house::hosting::add_to_waitlist;
呢?要想使用 use
将函数的父模块引入作用域,我们必须在调用函数时指定父模块,这样可以清晰地表明函数不是在本地定义的,而且可以避免和本地函数命名重复
另一方面,使用 use
引入结构体、枚举和其他项时,习惯是指定它们的完整路径,直接用。没什么原因,就是习惯用法。但是如果名称相同还是要引入父模块。
rust
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
}
fn function2() -> io::Result<()> {
}
也可使用 as
关键字提供新的名称
rust
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
}
fn function2() -> IoResult<()> {
}
如果想让外部调用你的代码的代码去调用 crate::front_of_house::hosting
可以加个 pub
。
rust
pub use crate::front_of_house::hosting;
使用外部包
rust
// 需要安装
use rand::Rng;
嵌套路径
rust
use std::cmp::Ordering;
use std::io;
// 化简
use std::{cmp::Ordering, io};
use std::io;
use std::io::Write;
// 化简
use std::io::{self, Write};
想要引入所有
rust
use std::collections::*;
以上所有的例子都在一个文件中定义多个模块。如何将模块分出去呢?
对于src/front_of_house.rs,在src/lib.rs,使用 mod
实现
rust
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
一个使用相对路径的例子
src/front_of_house.rs
rust
pub mod hosting;
src/front_of_house/hosting.rs
rust
pub fn add_to_waitlist() {}
集合
Vector
rust
// 声明,需要类型
let v: Vec<i32> = Vec::new();
// 直接利用vec宏来创建
let v = vec![1, 2, 3];
// 更新,需要用mut声明
let mut v = Vec::new();
v.push(5);
对于获取其元素,有两种方式。对于第一种方式,当使用一个不存在的元素时 Rust 会造成 panic,这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。对于第二个方式,当 get
方法被传递了一个数组外的索引时,它不会 panic 而是返回 None
。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它,接着代码可以有处理 Some
或 None
的逻辑。
rust
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
可能会遇到的错误。
rust
// 同时存在可变和不可变
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
为什么要用引用访问,下面的代码会报错,可以发现,不能仅仅把一个值的所有权转移,要转一起转。而对于整数这种,i32 实现了 Copy trait,所以取出来的是复制的值,所以不会有影响。所以还是加引用吧,因为你不需要拿到所有权。
rust
fn main() {
let a = String::from("hello");
let v = vec![a];
{
// 报错
let a = v[0];
// 可以这么写,不会报错
// let a = v;
println!("{}", a);
}
println!("{}", v[0]);
}
// 不会报错
fn main() {
let v = vec![1, 2, 3];
{
let a = v[0];
println!("{}", a);
}
println!("{}", v[0]);
}
遍历:
rust
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
// 更改值
let mut v = vec![100, 32, 57];
for i in &mut v {
// 引用的原值是无法改变的,需要解引用
*i += 50;
}
如何在 Vector 中存储多种不同类型的值,可以利用 enum
rust
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
字符串
新建字符串
rust
// 第一种方式
let mut s = String::new();
let data = "initial contents";
let s = data.to_string();
// 或者
let s = "initial contents".to_string();
// 第二种方式
let s = String::from("initial contents");
更新字符串
rust
let mut s = String::from("foo");
s.push_str("bar");
// s2还是能用的,因为push_str方法采用字符串slice,因为我们并不需要获取参数的所有权。
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {}", s2);
// 单独字符
let mut s = String::from("lo");
s.push('l');
// 拼接
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
// +必须这么用,第一个不加引用,后面的加
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
这里会出现个问题,下面的代码为什么 s 不会随着 s2 改变。这是因为 +
实际调用了 fn add(self, s: &str) -> String {
函数,里面会将s的值复制,而不是用它的引用。这里还有个问题,为什么 &String
能传进 add
中,是因为 &String
可以被 强转 (coerced )成 &str
,Rust 使用了一个被称为解引用强制多态(deref coercion)的技术,你可以将其理解为它把 &s2
变成了 &s2[..]
。
rust
let s1 = String::from("tic");
let mut s2 = String::from("toe");
let s = s1 + &s2;
s2 = "678".to_string();
println!("{}", s);
索引字符串
rust
// String不支持字符串索引
let s1 = String::from("hello");
let h = s1[0];
为啥不行,Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言,包括字节、标量值(不一定是字符,有的语音比较特别,会加语调)、字形簇(类似字符)。而且索引操作预期总是需要常数时间 (O(1))。但是对于 String
不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
字符串 slice
不好用
rust
let hello = "Здравствуйте";
let s = &hello[0..4];
// 实际输出为Зд
// 如果&hello[0..1],会报错
遍历
rust
for c in "नमस्ते".chars() {
println!("{}", c);
}
//न म स ् त े
for b in "नमस्ते".bytes() {
println!("{}", b);
}
// 224 164 165 135
// 从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate
哈希map
新建
rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// 另一个不常用的方法
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
所有权
rust
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
// 插入后会剥夺所有权
map.insert(field_name, field_value);
访问
rust
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name);
// 直接遍历
for (key, value) in &scores {
println!("{}: {}", key, value);
}
修改值
rust
// 直接覆盖
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
// 只在键没有对应值时插入
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Blue")).or_insert(50);
//
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
// or_insert会返回一个可变引用
// 你如果想用or_insert,就得用entry,它用于查找是否存在一个键
let count = map.entry(word).or_insert(0);
// 引用不能加减,要解引用
*count += 1;
}