Rust 学习
Rust 官网:https://www.rust-lang.org/zh-CN/
模块?库:https://crates.io/
1、Rust?简介
Rust 语言的主要目标之一是解决传统 系统级编程语言(如 C 和 C++)中常见的安全性问题,例如空指针引用、数据竞争等。为了实现这个目标,Rust 引入了一种称为 "所有权" 的概念,通过静态检查来确保内存安全和线程安全。此外,Rust 还具有其他一些特性,如模式匹配、代数数据类型、函数式编程风格的特性(如闭包和高阶函数)等。它还提供了丰富的标准库和包管理器 Cargo,使得开发者可以轻松构建和管理他们的项目。
Rust 是一门注重安全(safety)、速度(speed)和并发(concurrency)的现代系统编程语言。Rust 通过内存安全来实现以上目标,但不使用垃圾回收机制(garbage collection, GC)。
Rust 是?静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
Rust?特点
- 高性能:Rust 速度惊人且内存利用率极高。由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成。
- 可靠性:Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误。
- 生产力:Rust 拥有出色的文档、友好的编译器和清晰的错误提示信息, 还集成了一流的工具——包管理器和构建工具, 智能地自动补全和类型检验的多编辑器支持, 以及自动格式化代码等等。
Rust 相关概念
- channel:Rust?会发布3个不同版本:stable、beta、nightly。
? ? ? ? stable:Rust 的稳定版本,每 6 周发布一次。
? ? ? ? beta:Rust 的公开测试版本,将是下一个 stable 版本。
? ? ? ? nightly:每天更新,包含以一些实验性的新特性。 - toolchain:一套 Rust 组件,包括编译器及其相关工具,并且包含 channel,版本及支持的平台信息。
- target:指编译的目标平台,即:编译后的程序在哪种操作系统上运行。
- component (组件):toolchain 是由 component 组成的。查看所有可用和已经安装的组件命令如下:rustup component list。rustup 默认安装的组件:
? ? ? ? rustc:Rust 编译器。
? ? ? ? rust-std:Rust 标准库。
? ? ? ? cargo:包管理和构建工具。
? ? ? ? rust-docs:Rust 文档。
? ? ? ? rustfmt:用来格式化 Rust 源代码。
? ? ? ? clippy:Rust 的代码检查工具。 - profile:为了方便对 component 进行管理,使用 profile 定义一组 component。不同的 profile 包含不同的组件,安装 rustup 时有三种 profile 可选,修改 profile 命令如下:rustup set profile minimal
- Rustup?是什么:Rustup?是 Rust安装器和版本管理工具。安装 Rust 的主要方式是通过 Rustup 这一工具,它既是一个 Rust 安装器又是一个版本管理工具。Rust 的升级非常频繁。运行?
rustup update
?获取最新版本的 Rust。文档:https://rust-lang.github.io/rustup/ - Cargo 是什么:Cargo?是?Rust 的 构建工具 和 包管理器。安装?Rustup?时会自动安装。Cargo 可以做很多事情:
? ? ? ? cargo build? ? ? 可以构建项目
? ? ? ? cargo run? ? ? ? 可以运行项目
? ? ? ? cargo test? ? ? ?可以测试项目
? ? ? ? cargo doc? ? ? ?可以为项目构建文档
? ? ? ? cargo publish 可以将库发布到 crates.io。
检查是否安装了 Rust 和 Cargo,可以在终端中运行:cargo --version
下载、安装
下载:https://www.rust-lang.org/tools/install
安装:https://www.rust-lang.org/zh-CN/learn/get-started
默认情况,Rust 依赖 C++ build tools,没有安装也关系。安装过程需要保证网络正常。
在 Rust 开发环境中,所有工具都安装在 ~/.cargo/bin 目录中,可以在这里找到包括 rustc、cargo 和 rustup 在内的 Rust 工具链。在安装过程中,rustup 会尝试配置 PATH,如果 rustup 对 PATH 的修改不生效,可以手动添加路径到?PATH
~/.cargo/bin
~/.rustup/bin
以下是一些常用的命令:
rustup 相关
rustup -h ? ? ? ? ? ? ? ?# 查看帮助
rustup show ? ? ? ? ? ? ?# 显示当前安装的工具链信息
rustup update ? ? ? ? ? ?# 检查安装更新
rustup self uninstall ? ?# 卸载
rustup default stable-x86_64-pc-windows-gnu ? ?# 设置当前默认工具链rustup toolchain list ? ?# 查看工具链
rustup toolchain install stable-x86_64-pc-windows-gnu ? ?# 安装工具链
rustup toolchain uninstall stable-x86_64-pc-windows-gnu ?# 卸载工具链
rustup toolchain link <toolchain-name> "<toolchain-path>" ? ?# 设置自定义工具链rustup override list ? ?# 查看已设置的默认工具链
rustup override set <toolchain> --path <path> ? ?# 设置该目录以及其子目录的默认工具链
rustup override unset --path <path> ? ?# 取消目录以及其子目录的默认工具链rustup target list ? ? ? ? ? ? ? # 查看目标列表
rustup target add <target> ? ? ? # 安装目标
rustup target remove <target> ? ?# 卸载目标
rustup target add --toolchain <toolchain> <target> ? ?# 为特定工具链安装目标rustup component list ? ? ? ? ? ? ? ? # 查看可用组件
rustup component add <component> ? ? ?# 安装组件
rustup component remove <component> ? # 卸载组件
rustc 相关
rustc --version ? ?# 查看rustc版本
cargo 相关
cargo --version ? ?# 查看cargo版本
cargo new <project_name> ? ?# 新建项目
cargo build ? ?# 构建项目
cargo run ? ? ?# 运行项目
cargo check ? ?# 检查项目
cargo -h ? ? ? # 查看帮助
配置工具链安装位置
在系统环境变量中添加如下变量:
CARGO_HOME 指定 cargo 的安装目录
RUSTUP_HOME 指定 rustup 的安装目录
默认分别安装到用户目录下的.cargo 和.rustup 目录
配置国内镜像
配置 rustup 国内镜像。在系统环境变量中添加如下变量(选一个就可以,可以组合):
# 清华大学
RUSTUP_DIST_SERVER:https://mirrors.tuna.tsinghua.edu.cn/rustup
RUSTUP_UPDATE_ROOT:https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup# 中国科学技术大学
RUSTUP_DIST_SERVER:https://mirrors.ustc.edu.cn/rust-static
RUSTUP_UPDATE_ROOT:https://mirrors.ustc.edu.cn/rust-static/rustup
配置 cargo 国内镜像。在 cargo 安装目录下新建 config 文件(注意 config 没有任何后缀),文件内容如下:
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'tuna'# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"# 中国科学技术大学
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"
# 设置代理
[http]
proxy = "127.0.0.1:8889"
[https]
proxy = "127.0.0.1:8889"
Windows 交叉编译 Linux 程序。目标服务器是?Linux(CentOS 7) 64bit
, 所以我们添加的 target 应该是x86_64-unknown-linux-gnu
(动态依赖) 或者x86_64-unknown-linux-musl
(静态依赖)
- 动态依赖:目标服务器需要包含动态依赖的相关库(用户共享库)
- 静态依赖,目标服务器不需要包含相应的库,但是打包文件会更大些
1). 添加需要的 target
rustup target add ?x86_64-unknown-linux-musl
2). 在 cargo 安装目录下新建 config 文件(注意 config 没有任何后缀),添加的文件内容如下:
[target.x86_64-unknown-linux-musl]
linker = "rust-lld"
3). 构建
cargo build --target x86_64-unknown-linux-musl
示例:
创建新项目
用 Cargo 创建一个新项目。在终端中执行:cargo new hello-rust,会生成一个名为?hello-rust
?的新目录,其中包含以下文件:
Cargo.toml
?为 Rust 的清单文件。其中包含了项目的元数据和依赖库。src/main.rs
?为编写应用代码的地方。
进入新创建的目录中,执行命令运行此程序:cargo run
添加 依赖
现在来为程序添加依赖。可以在?crates.io,即 Rust 包的仓库中找到所有类别的库。在 Rust 中通常把 "包" 称作 "crates"。在本项目中,使用了名为?ferris-says?的库。
在?Cargo.toml
?文件中添加以下信息(从 crate 页面上获取):
[dependencies]
ferris-says = "0.3.1"
下载 依赖
运行:cargo build? ,?Cargo 就会安装该依赖。运行?build?会创建一个新文件?
Cargo.lock
,该文件记录了本地所用依赖库的精确版本。
使用?依赖
使用该依赖库:可以打开?
main.rs
,删除其中所有的内容(它不过是个示例而已),然后在其中添加下面这行代码:use ferris_says::say;这样就可以使用?
ferris-says
?crate 中导出的?say
?函数了。
完整?Rust?示例
现在用上面的依赖库编写一个小应用。在?main.rs
?中添加以下代码:
use ferris_says::say; // from the previous step
use std::io::{stdout, BufWriter};
fn main() {
let stdout = stdout();
let message = String::from("Hello fellow Rustaceans!");
let width = message.chars().count();
let mut writer = BufWriter::new(stdout.lock());
say(&message, width, &mut writer).unwrap();
}
保存完毕后,执行命令运行程序:cargo run
成功执行后,会打印一个字符形式的螃蟹图案。
Ferris (?费理斯 )?是 Rust 社区的非官方吉祥物。
2、Rust?相关文档
:https://www.rust-lang.org/zh-CN/learn
核心文档
以下所有文档都可以用?rustup doc
?命令在本地阅读,它会在浏览器中离线打开这些资源!
标准库
详尽的 Rust 标准库 API 手册。:https://doc.rust-lang.org/std/index.html
版本指南
Rust 版本指南。:https://doc.rust-lang.org/edition-guide/index.html
CARGO 手册
Rust 的包管理器和构建系统。:https://doc.rust-lang.org/cargo/index.html
RUSTDOC 手册
学习如何为?crate 编写完美的文档。:https://doc.rust-lang.org/rustdoc/index.html
RUSTC 手册
熟悉 Rust 编译器中可用的选项。:https://doc.rust-lang.org/rustc/index.html
编译错误索引表
深入解释遇到的编译错误。:https://doc.rust-lang.org/error_codes/error-index.html
Rust?程序
命令行?程序
用 Rust 构建高效的命令行应用。:https://rust-cli.github.io/book/index.html
WEBASSEMBLY 手册
通过 WebAssembly 用 Rust 构建浏览器原生的库。:https://rustwasm.github.io/docs/book/
嵌入式手册
Rust 编写嵌入式程序。:https://doc.rust-lang.org/stable/embedded-book/
Learn X?in Y
// 这是注释,单行注释...
/* ...这是多行注释 */
///
// 1. 基础 //
///
// 函数 (Functions)
// `i32` 是有符号 32 位整数类型(32-bit signed integers)
fn add2(x: i32, y: i32) -> i32 {
// 隐式返回 (不要分号)
x + y
}
// 主函数(Main function)
fn main() {
// 数字 (Numbers) //
// 不可变绑定
let x: i32 = 1;
// 整形/浮点型数 后缀
let y: i32 = 13i32;
let f: f64 = 1.3f64;
// 类型推导
// 大部分时间,Rust 编译器会推导变量类型,所以不必把类型显式写出来。
// 这个教程里面很多地方都显式写了类型,但是只是为了示范。
// 绝大部分时间可以交给类型推导。
let implicit_x = 1;
let implicit_f = 1.3;
// 算术运算
let sum = x + y + 13;
// 可变变量
let mut mutable = 1;
mutable = 4;
mutable += 2;
// 字符串 (Strings) //
// 字符串字面量
let x: &str = "hello world!";
// 输出
println!("{} {}", f, x); // 1.3 hello world
// 一个 `String` – 在堆上分配空间的字符串
let s: String = "hello world".to_string();
// 字符串分片(slice) - 另一个字符串的不可变视图
// 基本上就是指向一个字符串的不可变指针,它不包含字符串里任何内容,只是一个指向某个东西的指针
// 比如这里就是 `s`
let s_slice: &str = &s;
println!("{} {}", s, s_slice); // hello world hello world
// 数组 (Vectors/arrays) //
// 长度固定的数组 (array)
let four_ints: [i32; 4] = [1, 2, 3, 4];
// 变长数组 (vector)
let mut vector: Vec<i32> = vec![1, 2, 3, 4];
vector.push(5);
// 分片 - 某个数组(vector/array)的不可变视图
// 和字符串分片基本一样,只不过是针对数组的
let slice: &[i32] = &vector;
// 使用 `{:?}` 按调试样式输出
println!("{:?} {:?}", vector, slice); // [1, 2, 3, 4, 5] [1, 2, 3, 4, 5]
// 元组 (Tuples) //
// 元组是固定大小的一组值,可以是不同类型
let x: (i32, &str, f64) = (1, "hello", 3.4);
// 解构 `let`
let (a, b, c) = x;
println!("{} {} {}", a, b, c); // 1 hello 3.4
// 索引
println!("{}", x.1); // hello
//
// 2. 类型 (Type) //
//
// 结构体(Sturct)
struct Point {
x: i32,
y: i32,
}
let origin: Point = Point { x: 0, y: 0 };
// 匿名成员结构体,又叫“元组结构体”(‘tuple struct’)
struct Point2(i32, i32);
let origin2 = Point2(0, 0);
// 基础的 C 风格枚举类型(enum)
enum Direction {
Left,
Right,
Up,
Down,
}
let up = Direction::Up;
// 有成员的枚举类型
enum OptionalI32 {
AnI32(i32),
Nothing,
}
let two: OptionalI32 = OptionalI32::AnI32(2);
let nothing = OptionalI32::Nothing;
// 泛型 (Generics) //
struct Foo<T> { bar: T }
// 这个在标准库里面有实现,叫 `Option`
enum Optional<T> {
SomeVal(T),
NoVal,
}
// 方法 (Methods) //
impl<T> Foo<T> {
// 方法需要一个显式的 `self` 参数
fn get_bar(self) -> T {
self.bar
}
}
let a_foo = Foo { bar: 1 };
println!("{}", a_foo.get_bar()); // 1
// 接口(Traits) (其他语言里叫 interfaces 或 typeclasses) //
trait Frobnicate<T> {
fn frobnicate(self) -> Option<T>;
}
impl<T> Frobnicate<T> for Foo<T> {
fn frobnicate(self) -> Option<T> {
Some(self.bar)
}
}
let another_foo = Foo { bar: 1 };
println!("{:?}", another_foo.frobnicate()); // Some(1)
///
// 3. 模式匹配 (Pattern matching) //
///
let foo = OptionalI32::AnI32(1);
match foo {
OptionalI32::AnI32(n) => println!("it’s an i32: {}", n),
OptionalI32::Nothing => println!("it’s nothing!"),
}
// 高级模式匹配
struct FooBar { x: i32, y: OptionalI32 }
let bar = FooBar { x: 15, y: OptionalI32::AnI32(32) };
match bar {
FooBar { x: 0, y: OptionalI32::AnI32(0) } =>
println!("The numbers are zero!"),
FooBar { x: n, y: OptionalI32::AnI32(m) } if n == m =>
println!("The numbers are the same"),
FooBar { x: n, y: OptionalI32::AnI32(m) } =>
println!("Different numbers: {} {}", n, m),
FooBar { x: _, y: OptionalI32::Nothing } =>
println!("The second number is Nothing!"),
}
///
// 4. 流程控制 (Control flow) //
///
// `for` 循环
let array = [1, 2, 3];
for i in array {
println!("{}", i);
}
// 区间 (Ranges)
for i in 0u32..10 {
print!("{} ", i);
}
println!("");
// 输出 `0 1 2 3 4 5 6 7 8 9 `
// `if`
if 1 == 1 {
println!("Maths is working!");
} else {
println!("Oh no...");
}
// `if` 可以当表达式
let value = if true {
"good"
} else {
"bad"
};
// `while` 循环
while 1 == 1 {
println!("The universe is operating normally.");
}
// 无限循环
loop {
println!("Hello!");
}
// 5. 内存安全和指针 (Memory safety & pointers) //
// 独占指针 (Owned pointer) - 同一时刻只能有一个对象能“拥有”这个指针
// 意味着 `Box` 离开他的作用域后,会被安全地释放
let mut mine: Box<i32> = Box::new(3);
*mine = 5; // 解引用
// `now_its_mine` 获取了 `mine` 的所有权。换句话说,`mine` 移动 (move) 了
let mut now_its_mine = mine;
*now_its_mine += 2;
println!("{}", now_its_mine); // 7
// println!("{}", mine); // 编译报错,因为现在 `now_its_mine` 独占那个指针
// 引用 (Reference) – 引用其他数据的不可变指针
// 当引用指向某个值,我们称为“借用”这个值,因为是被不可变的借用,所以不能被修改,也不能移动
// 借用一直持续到生命周期结束,即离开作用域
let mut var = 4;
var = 3;
let ref_var: &i32 = &var;
println!("{}", var); //不像 `mine`, `var` 还可以继续使用
println!("{}", *ref_var);
// var = 5; // 编译报错,因为 `var` 被借用了
// *ref_var = 6; // 编译报错,因为 `ref_var` 是不可变引用
// 可变引用 (Mutable reference)
// 当一个变量被可变地借用时,也不可使用
let mut var2 = 4;
let ref_var2: &mut i32 = &mut var2;
*ref_var2 += 2;
println!("{}", *ref_var2); // 6
// var2 = 2; // 编译报错,因为 `var2` 被借用了
}
rust?打印占位符
在 Rust 中,打印的占位符由格式化宏提供,最常用的是 println!
和 format!
。下面是一些常见的占位符及其用法:
-
{}
:默认占位符,根据值的类型自动选择合适的显示方式。 -
{:?}
:调试占位符,用于打印调试信息。通常用于Debug
trait 的实现。 -
{:#?}
:类似于{:?}
,但打印出更具可读性的格式化调试信息,可以嵌套显示结构体和枚举的字段。 -
{x}
:将变量x
的值插入到占位符的位置。 -
{x:format}
:将变量x
按照指定的格式进行格式化输出。例如,{x:?}",
{x:b},
{x:e}` 等。
这只是一小部分常见的占位符用法,你还可以根据需要使用其他格式化选项。Rust 的格式化宏提供了非常灵活和强大的格式化功能,可以满足大多数打印需求。
fn main() {
let name = "Alice";
let age = 25;
let height = 1.65;
println!("Name: {}", name);
println!("Age: {}", age);
println!("Height: {:.2}", height); // 格式化为小数点后两位
let point = (3, 5);
println!("Point: {:?}", point);
}
打印 "枚举、结构体"
#[derive(Debug)]
enum MyEnum {
Variant1,
Variant2(u32),
Variant3 { name: String, age: u32 },
}
#[derive(Debug)]
struct MyStruct{
field_1: String,
field_2: usize,
}
impl MyStruct {
fn init_field(&self){
let name = &self.field_1;
let age = self.field_2;
println!("{name} ---> {age}")
}
}
fn main() {
let my_enum = MyEnum::Variant2(42);
println!("{:?}", my_enum);
let my_struct = MyStruct{
field_1: String::from("king"),
field_2: 100
};
my_struct.init_field();
println!("{:?}", my_struct);
}
3、Rust 程序设计语言
英文文档:https://doc.rust-lang.org/book/
中文文档:https://kaisery.github.io/trpl-zh-cn/
《Rust 程序设计语言》被亲切地称为“圣经”。给出了 Rust 语言的概览。在阅读的过程中构建几个项目,读完后,就能扎实地掌握 Rust 语言。
- 1.?入门指南
- 2.?写个猜数字游戏
- 3.?常见编程概念
- 4.?认识所有权
- 5.?使用结构体组织相关联的数据
- 6.?枚举和模式匹配
- 7.?使用包、Crate 和模块管理不断增长的项目
- 8.?常见集合
- 9.?错误处理
- 10.?泛型、Trait 和生命周期
- 11.?编写自动化测试
- 12.?一个 I/O 项目:构建命令行程序
- 13.?Rust 中的函数式语言功能:迭代器与闭包
- 14.?更多关于 Cargo 和 Crates.io 的内容
- 15.?智能指针
- 16.?无畏并发
- 17.?Rust 的面向对象编程特性
- 18.?模式与模式匹配
- 19.?高级特征
- 20.?最后的项目:构建多线程 web server
- 21.?附录
Rust?数据类型
Rust 是?静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
标量、复合
Rust 2大类数据类型:
- 标量(scalar):代表一个单独的值。4种基本的标量:整型、浮点型、布尔类型、字符。字符串(
String
)类型由 Rust 标准库提供,而不是编入核心语言。在 Rust 中,"字符串字面值" 使用双引号括起来,例如:"Hello, World!"。这是一种字符串类型的常量表示方法。而普通的字符串类型则是指动态可变的字符串,即 String 类型。字符串字面值是静态不可变的,不能修改其中的内容。你可以直接使用字符串字面值进行一些简单的操作,如拼接、切割等,但无法修改它们的值。字符串字面值就是 String 的 slice - 复合(compound):Rust 有两个原生的复合类型:元组(tuple)、数组(array)。
元组 是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。
另一个包含多个值的方式是 数组(array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,Rust 中的数组长度是固定的。
Rust 标准库中包含一系列被称为?集合(collections)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。
三个在 Rust 程序中被广泛使用的集合:
- vector??一个挨着一个地储存一系列数量可变的值。为了创建一个新的空 vector,可以调用 Vec::new 函数,会用初始值来创建一个 Vec<T> 而 Rust 会推断出储存值的类型,所以很少会需要这些类型注解。为了方便 Rust 提供了 vec! 宏,这个宏会根据我们提供的值来创建一个新的 vector。
- 字符串(string)是字符的集合。我们之前见过?
String
?类型,不过在本章我们将深入了解。 - 哈希 map(hash map)允许我们将值与一个特定的键(key)相关联。这是一个叫做?map?的更通用的数据结构的特定实现。
对于标准库提供的其他类型的集合,请查看文档。
vec?示例:
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
println!("{:?}", v);
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
// 为了修改可变引用所指向的值,在使用 += 运算符之前
// 必须使用解引用运算符(*)获取 i 中的值。
*i += 100;
}
for i in &v{
println!("{i}")
}
}
示例:
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
println!("{:?}", v);
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 100;
}
for i in &v{
println!("{i}")
}
#[derive(Debug)]
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
// 使用枚举来存储多个类型,类比 Python 的 list
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
for cell in &row {
match cell {
SpreadsheetCell::Int(value) => println!("整数值: {}", value),
SpreadsheetCell::Text(value) => println!("文本值: {}", value),
SpreadsheetCell::Float(value) => println!("浮点数值: {}", value),
}
}
}
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加?match
?意味着 Rust 能在编译时就保证总是会处理所有可能的情况,
如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,
字符串
Rust 的核心语言中只有一种字符串类型:字符串 slice?str
,它通常以被借用的形式出现,&str
。第四章讲到了?字符串 slices:它们是一些对储存在别处的 UTF-8 编码字符串数据的引用。举例来说,由于字符串字面值被储存在程序的二进制输出中,因此字符串字面值也是字符串 slices。
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
println!("s2 ---> {s2}");
println!("s3 ---> {s3}");
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
// 宏 format! 生成的代码使用引用所以不会获取任何参数的所有权。
let s = format!("{s1}-{s2}-{s3}");
索引字符串
println!("s ---> {s}");
println!("s1 ---> {s1}");
println!("s2 ---> {s2}");
println!("s3 ---> {s3}");
}
哈希 map:HashMap<K, V>
HashMap<K, V>
?类型储存了一个键类型?K
?对应一个值类型?V
?的映射。它通过一个?哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表、关联数组、Python的字典(Dict)?等。
可以使用?new
?创建一个空的?HashMap
,并使用?insert
?增加元素。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
必须首先?use
?标准库中集合部分的?HashMap
。在这三个常用集合中,HashMap
?是最不常用的,所以并没有被 prelude 自动引用。标准库中对?HashMap
?的支持也相对较少,例如,并没有内建的构建宏。类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
Rust?的?所有权
:https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html
所有权(系统)是 Rust 最为与众不同的特性,对语言的其他部分有着深刻含义。它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全,因此理解 Rust 中所有权如何工作是十分重要的。
所有权的规则
- Rust 中的每一个值都有一个?所有者(owner)。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被 丢弃(释放内存空间)。
变量、作用域
变量是否有效与作用域的关系跟其他编程语言是类似的。
fn main() {
? ? { ? ? ? ? ? ? ? ? ? ? ?// s 在这里无效,它尚未声明
? ? ? ? let s = "hello"; ? // 从此处起,s 是有效的
? ? ? ? // 使用 s
? ? } ? ? ? ? ? ? ? ? ? ? ?// 此作用域已结束,s 不再有效,清理并drop(释放掉)内存空间
}这里有两个重要的时间点:
- 当?
s
?进入作用域?时,它就是有效的。- 这一直持续到它?离开作用域?为止。
变量与数据交互:移动 (浅拷贝)、克隆 (深拷贝)
- 浅拷贝(shallow copy)和?深拷贝(deep copy)
浅拷贝:拷贝指针、长度和容量,而不拷贝指针所指向内存空间的数据。
深拷贝:拷贝指针、长度和容量,同时也拷贝指针所指向内存空间的数据。
fn main() {
? ? let s1 = String::from("hello");
? ? let s2 = s1;? ? ? ? ? // 在 C++ 中,这里会发生浅拷贝,不过在?Rust?中会使第一个变量无效,这个操作被称为?移动(move),而不是叫做浅拷贝。为了确保内存安全,在?let s2 = s1;
?之后,Rust 认为?s1
?不再有效,因此 Rust 不需要在?s1
?离开作用域后清理任何东西。? ??println!("{}, world!", s1);? ? //?看看在?
s2
?被创建之后尝试使用?s1
?会发生什么,这里会报错,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 因为只有?s2
?是有效的,当其离开作用域,它就释放自己的内存
}这里隐含了 Rust?的一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。
变量的 "移动(转移)"
只要进行 "赋值(=)、函数传参"?都会有?移动
- 移动 ( 转移 ):变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
变量的?克隆
确实?需要深度复制?
String
?中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做?clone
?的通用函数。fn main() {
? ? let s1 = String::from("hello");
? ? let s2 = s1.clone();? ? println!("s1 = {}, s2 = {}", s1, s2);
}
只在栈上的数据:拷贝
fn main() {
? ? let x = 5;
? ? let y = x;? ? println!("x = {}, y = {}", x, y);
}一个通用的规则,任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。如下是一些 Copy 的类型:
- 所有整数类型,比如?
u32
。- 布尔类型,
bool
,它的值是?true
?和?false
。- 所有浮点数类型,比如?
f64
。- 字符类型,
char
。- 元组,当且仅当其包含的类型也都实现?
Copy
?的时候。比如,(i32, i32)
?实现了?Copy
,但?(i32, String)
?就没有。
所有权与函数
将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
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 移出作用域。没有特殊之处
?
返回值与作用域
返回值也可以转移所有权。
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("yours"); // 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)像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。 与指针不同,引用确保指向某个特定类型的有效值。
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 是 String 的引用
? ? s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
? // 所以什么也不会发生变量?
s
?有效的作用域与函数参数的作用域一样,不过当?s
?停止使用时并不丢弃引用指向的数据,因为?s
?并没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
- 总结:将创建一个引用的行为称为?借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。因为并不拥有它。
正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。
如果想要修改引用的值,就需要用到?可变引用(mutable reference)。可变引用有一个很大的限制:如果创建了一个变量的可变引用,就不能再创建对该变量的引用。不可变引用的值本身就不希望被改变,一个变量可以有多个不可变引用。
fn main() {
? ? let mut s = String::from("hello");? ? let r1 = &mut s;
? ? let r2 = &mut s;? ? println!("{}, {}", r1, r2);
}
这个报错说这段代码是无效的,因为我们不能在同一时间多次将?s
?作为可变变量借用。第一个可变的借入在?r1
?中,并且必须持续到在?println!
?中使用它,但是在那个可变引用的创建和它的使用之间,我们又尝试在?r2
?中创建另一个可变引用,该引用借用与?r1
?相同的数据。这一限制以一种非常小心谨慎的方式允许可变性,防止同一时间对同一数据存在多个可变引用。新 Rustacean 们经常难以适应这一点,因为大部分语言中变量任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能?同时?拥有:
fn main() {
? ? let mut s = String::from("hello");? ? {
? ? ? ? let r1 = &mut s;
? ? } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用? ? let r2 = &mut s;
}
Rust 在同时使用可变与不可变引用时也采用的类似的规则。这些代码会导致一个错误:fn main() {
? ? let mut s = String::from("hello");? ? let r1 = &s; // 没问题
? ? let r2 = &s; // 没问题
? ? let r3 = &mut s; // 大问题? ? println!("{}, {}, and {}", r1, r2, r3);? ?
}? ?// r1、r2、r3?作用域都是到这里结束,但是上面打印时,r1、r2?生效时 r3?也生效,所以报错。因为?rust?会自动判断?变量引用的作用域是否重叠,所以可以调整?println?的顺序即可。fn main() {
? ? let mut s = String::from("hello");? ? let r1 = &s; // 没问题
? ? let r2 = &s; // 没问题? ? println!("{}, {}", r1, r2);
? ? let r3 = &mut s; // 大问题
? ? println!("{}", r3);
}不可变引用?
r1
?和?r2
?的作用域在?println!
?最后一次使用之后结束,这也是创建可变引用?r3
?的地方。它们的作用域没有重叠,所以代码是可以编译的。编译器可以在作用域结束之前判断不再使用的引用。记住这是 Rust 编译器在提前指出一个潜在的 bug?的规定。
悬垂引用(Dangling References)
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个?悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
fn main() {
? ? let reference_to_nothing = dangle();
}fn dangle() -> &String { // dangle 返回一个字符串的引用
? ? let s = String::from("hello"); // s 是一个新字符串
? ? &s // 返回字符串 s 的引用,但是引用不转移所有权,所以函数结束时,s?被销毁释放内存
} // 这里 s 离开作用域并被丢弃。其内存被释放。
? // 危险!
正确的做法:不返回引用。fn main() {
? ? let string = no_dangle();
}fn no_dangle() -> String {
? ? let s = String::from("hello");
? ? s? ?//?这样就没有任何错误了。所有权被移动出去,所以没有值被释放。
}
Slice 类型:slice?允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一类引用,所以它同样没有所有权。
枚举、结构体
- 枚举(enumerations),也被称作 enums。枚举允许你通过列举可能的 成员(variants)来定义一个类型。
- struct,或者 structure,是一个自定义数据类型,允许你包装和命名多个相关的值,从而形成一个有意义的组合。结构体可以定义方法。结构体作用就是将字段和数据聚合在一块,形成新的数据类型。
fn main() {
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
match home {
IpAddr::V4(ip) => println!("Home IPv4 地址是: {}", ip),
IpAddr::V6(ip) => println!("Home IPv6 地址是: {}", ip),
}
match loopback {
IpAddr::V4(ip) => println!("Loopback IPv4 地址是: {}", ip),
IpAddr::V6(ip) => println!("Loopback IPv6 地址是: {}", ip),
}
}
Rust?的?Result
在 Rust 中,Result
是一个枚举类型,它代表了可能产生错误的操作的结果。Result
枚举有两个变体:Ok
和 Err
。
Ok
变体表示操作成功,并包含操作返回的值。Err
变体表示操作失败,并包含一个错误值,用于描述错误的原因。
通常,Result
类型被用于表示可能会发生错误的函数的返回类型。这样,调用者可以通过检查 Result
来处理操作的成功或失败。简单的示例,演示如何使用 Result
:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err(String::from("除数不能为零"));
}
Ok(a / b)
}
fn main() {
let result = divide(10, 2);
match result {
Ok(value) => println!("结果是: {}", value),
Err(error) => println!("出现错误: {}", error),
}
}
示例 2:
use std::io;
use std::io::stdin;
fn main() {
let mut input_str = String::from("");
stdin().read_line(&mut input_str).expect("获取输入失败");
let input_int:usize = match input_str.trim().parse() {
Ok(n) => n,
Err(_) => {
println!("无效的输入");
return;
}
};
let result = input_int * 100;
println!("{result}")
}
match 控制流结构、匹配 Option<T>
- Rust?的?
match
?是极为强大的控制流运算符,它允许将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成。 - ?
Option<T>
?时,是为了从?Some
?中取出其内部的?T
?值;
? ? ? ? 如果其中含有一个值,则执行有值得流程。
? ? ? ? 如果其中没有值,则执行没有值得流程。
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
// 使用模式匹配
match six {
Some(value) => println!("Some 值是: {}", value),
None => println!("None"),
}
// 使用 unwrap() 方法
if let Some(value) = five {
println!("Some 值是: {}", value);
} else {
println!("None");
}
if let Some(value) = six {
println!("Some 值是: {}", value);
} else {
println!("None");
}
}
使用包、Crate、模块管理
Rust 有许多功能可以让你管理代码的组织,包括哪些内容可以被公开,哪些内容作为私有部分,以及程序每个作用域中的名字。这些功能,有时被统称为 “模块系统(the module system)”,包括:
- crate :是 Rust 在编译时最小的代码单位。如果用?
rustc
?而不是?cargo
?来编译一个文件时,编译器会将那个文件认作一个 crate。crate 可以包含模块,模块可以定义在其他文件,然后和 crate 一起编译。crate 有两种形式:二进制的可执行程序、lib库。 - Crates?:一个模块的树形结构,它形成了库或二进制项目。
- 包(Packages):Cargo 的一个功能,它允许你构建、测试和分享 crate。
包(package)是提供一系列功能的一个或者多个 crate。一个包会包含一个?Cargo.toml?文件,阐述如何去构建这些 crate。Cargo 就是一个包含构建你代码的二进制项的包。Cargo 也包含这些二进制项所依赖的库。其他项目也能用 Cargo 库来实现与 Cargo 命令行程序一样的逻辑。包中可以包含至多一个库 crate(library crate)。包中可以包含任意多个二进制 crate(binary crate),但是必须至少包含一个 crate(无论是库的还是二进制的)。
- 从 crate 根节点开始:当编译一个 crate,编译器首先在 crate 根文件(通常,对于一个库 crate 而言是?src/lib.rs,对于一个二进制 crate 而言是?src/main.rs)中寻找需要被编译的代码。
- 声明模块: 在 crate 根文件中,你可以声明一个新模块;比如,你用
mod garden
声明了一个叫做garden
的模块。编译器会在下列路径中寻找模块代码:- 内联,在大括号中,当
mod garden
后方不是一个分号而是一个大括号 - 在文件?src/garden.rs
- 在文件?src/garden/mod.rs
- 内联,在大括号中,当
- 声明子模块: 在除了 crate 根节点以外的其他文件中,你可以定义子模块。比如,你可能在src/garden.rs中定义了
mod vegetables;
。编译器会在以父模块命名的目录中寻找子模块代码:- 内联,在大括号中,当
mod vegetables
后方不是一个分号而是一个大括号 - 在文件?src/garden/vegetables.rs
- 在文件?src/garden/vegetables/mod.rs
- 内联,在大括号中,当
- 模块中的代码路径: 一旦一个模块是你 crate 的一部分,你可以在隐私规则允许的前提下,从同一个 crate 内的任意地方,通过代码路径引用该模块的代码。举例而言,一个 garden vegetables 模块下的
Asparagus
类型可以在crate::garden::vegetables::Asparagus
被找到。 - 私有 vs 公用: 一个模块里的代码默认对其父模块私有。为了使一个模块公用,应当在声明时使用
pub mod
替代mod
。为了使一个公用模块内部的成员公用,应当在声明前使用pub
。 use
?关键字: 在一个作用域内,use
关键字创建了一个成员的快捷方式,用来减少长路径的重复。在任何可以引用crate::garden::vegetables::Asparagus
的作用域,你可以通过?use crate::garden::vegetables::Asparagus;
创建一个快捷方式,然后你就可以在作用域中只写Asparagus
来使用该类型。
示例:创建一个名为backyard
的二进制 crate 来说明这些规则。该 crate 的路径同样命名为backyard
,该路径包含了这些文件和目录:
这个例子中的 crate 根文件是src/main.rs,该文件包括了:
文件名:src/main.rs
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
? ? let plant = Asparagus {};
? ? println!("I'm growing {:?}!", plant);
}
pub mod garden;
行告诉编译器应该包含在?src/garden.rs?文件中发现的代码:
文件名:src/garden.rs
pub mod vegetables;
在此处,?pub mod vegetables;
意味着在src/garden/vegetables.rs中的代码也应该被包括。这些代码是:
#[derive(Debug)]
pub struct Asparagus {}
模块的路径有两种形式:
- 绝对路径(absolute path)是以 crate 根(root)开头的全路径;对于外部 crate 的代码,是以 crate 名开头的绝对路径,对于当前 crate 的代码,则以字面值?
crate
?开头。 - 相对路径(relative path)从当前模块开始,以?
self
、super
?或当前模块的标识符开头。
绝对路径和相对路径都后跟一个或多个由双冒号(::
)分割的标识符。
4、通过例子学 Rust
英文文档:https://doc.rust-lang.org/rust-by-example/
中文文档:https://rustwiki.org/zh-CN/rust-by-example/
查看更多?Rust 官方文档中英文双语教程,包括双语版《Rust 程序设计语言》(出版书名为《Rust 权威指南》),?Rust 标准库中文版。
《通过例子学 Rust》(Rust By Example 中文版)翻译自?Rust By Example,中文版最后更新时间:2022-1-26。查看此书的?Github 翻译项目和源码。
开始学习吧!
-
Hello World?- 从经典的 “Hello World” 程序开始学习。
-
原生类型?- 学习有符号整型,无符号整型和其他原生类型。
-
自定义类型?- 结构体?
struct
?和 枚举?enum
。 -
变量绑定?- 变量绑定,作用域,变量遮蔽。
-
类型系统?- 学习改变和定义类型。
-
流程控制?-?
if
/else
,for
,以及其他流程控制有关内容。 -
函数?- 学习方法、闭包和高阶函数。
-
模块?- 使用模块来组织代码。
-
Crate?- crate 是 Rust 中的编译单元。学习创建一个库。
-
Cargo?- 学习官方的 Rust 包管理工具的一些基本功能。
-
属性?- 属性是应用于某些模块、crate 或项的元数据(metadata)。
-
泛型?- 学习编写能够适用于多种类型参数的函数或数据类型。
-
作用域规则?- 作用域在所有权(ownership)、借用(borrowing)和生命周期(lifetime)中起着重要作用。
-
特性 trait?- trait 是对未知类型(
Self
)定义的方法集。 -
错误处理?- 学习 Rust 语言处理失败的方式。
-
标准库类型?- 学习?
std
?标准库提供的一些自定义类型。 -
标准库更多介绍?- 更多关于文件处理、线程的自定义类型。
-
测试?- Rust 语言的各种测试手段。
-
补充?- 文档和基准测试
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!