文章时效性提示
这是一篇发布于 301 天前的文章,部分信息可能已发生改变,请注意甄别。
Rust官方推荐的三个资料,分别是The Rust programming language,Rust by examples以及ruslings,已经相当充足了.包括相对全面的书,代码例子以及方便的互动式exercises.个人觉得,the book相当于字典,虽然其实还有内容更多的reference,而examples更加易懂上手,rustlings相当于刷题,把关键东西了解一遍.
所以从这三个东西入手开始Rust学习之旅,一些地方会跟c++对比.
宏macro
术语宏指的是Rust中的一系列特性:带有macro_rules的声明性宏!还有三种过程宏:
- Custom
#[derive]
macros that specify code added with thederive
attribute used on structs and enums - Attribute-like macros that define custom attributes usable on any item
- Function-like macros that look like function calls but operate on the tokens specified as their argument
1 |
|
变量binding
知识点:
- 不变性 Rust默认不可变,不像c++到处声明const
- scope和shadowing 主要有variable shadowing,也就是可以重声明,在c++中不允许
- 将一个mut的值赋值给non-mut的值,在那个域内,non-mute的值也不能被改变
1 | fn main() { |
Primitives
字面量和操作符
- 推荐在使用字面量是后面加上类型.
- 原生类型本身可以printable
元组、数组与slices.
元组,通过()表示,通过.num索引,可以使用#[derive(Debug)]实现方法打印.
数组 [T;length]声明,编译时已知.
切片(Slices)与数组类似,但它们的长度在编译时是未知的。相反,切片是一个由两个字(word)组成的对象:第一个字是指向数据的指针,第二个字是切片的长度。字的大小与 usize
类型相同,由处理器架构决定,例如在 x86-64 上为 64 位。切片可用于借用数组的一部分,它的类型签名为 &[T]
。
1 | // This function borrows a slice. |
自定义类型
structures
使用 struct
关键字可以创建三种类型的结构体(struct):
- 元组结构体(Tuple structs),基本上是命名元组。
- 经典的 C 风格结构体。
- 无字段的单元结构体(Unit structs),在泛型编程中很有用。
1 |
|
Enums
enum
关键字允许创建一个可以是几种不同变体(variant)之一的类型。任何在结构体(struct)中有效的变体,在枚举(enum)中也是有效的。
1 | enum IpAddrKind { |
constants
Rust 有两种不同类型的常量,可以在任何作用域(包括全局)中声明。两种常量都需要显式的类型注解:
const
: 不可变的值(最常见的情况)。static
: 可能是可变的变量,拥有'static
生命周期。'static
生命周期是被推断出来的,不需要显式指定。访问或修改可变的static
变量是不安全的
工具
强大的包管理系统
一个rust项目可能是lib也可以是main,分别代表生成库和可执行程序. 注意可以既存在main.rs和lib.rs.
- Packages: A Cargo feature that lets you build, test, and share crates
- Crates: A tree of modules that produces a library or executable
- Modules and use: Let you control the organization, scope, and privacy of paths
- Paths: A way of naming an item, such as a struct, function, or module
crate根文件是Rust编译器启动的源文件,它构成了crate的根模块
包(Packages)是提供一组功能的一个或多个crate的集合。一个包包含一个Cargo.toml。描述如何构建这些crate的Toml文件。Cargo实际上是一个包,其中包含用于构建代码的命令行工具的二进制crate。Cargo包还包含一个二进制包所依赖的库包。其他项目可以依赖Cargo库crate来使用Cargo命令行工具使用的相同逻辑。
crate有两种形式:二进制(binary)crate或库(library )crate。二进制crate是可以编译为可运行的可执行文件的程序,例如命令行程序或服务器。每个程序都必须有一个名为main的函数,用于定义可执行程序运行时发生的情况。
library crate没有main函数,也不能编译成可执行文件。相反,它们定义了旨在与多个项目共享的功能。
项目结构
从crate根目录开始:当编译一个crate时,编译器首先查找crate根文件(通常是src/lib.rs,为库crate或src/main.rs表示二进制文件)用于编译代码。
声明模块:在crate根文件中,你可以声明新的模块;假设你用mod garden;声明了一个“花园”模块。编译器将在这些地方查找模块的代码:
内联下载mod garden之后
在src/garden.rs文件中
在src/garden/mod.rs文件中 查找模块的规律就是同级目录同名文件或者同名子目录中的
mod.rs
文件声明子模块:在crate根目录之外的任何文件中(除了
src/main/lib.rs
的文件中),都可以声明子模块。例如,你可以声明mod vegetables;在src/garden.rs
。编译器将在以下地方以父模块命名的目录中查找子模块的代码:内联,直接跟在mod vegetables后面
在src/garden/vegetables.rs文件中
在src/garden/vegetables/mod.rs文件中 查找子模块的规律就是与文件同名子目录中的同名模块文件或者同名模块目录中的
mod.rs
文件模块中的代码路径:一旦模块成为crate的一部分,只要隐私规则允许,就可以使用代码路径从同一crate中的任何其他地方引用该模块中的代码。例如,garden vegetables模块中的Asparagus类型可以在crate::garden::vegetables::Asparagus中找到。
私有vs.公共:默认情况下,模块内的代码对其父模块是私有的。要使一个模块为公共,请使用pub mod而不是mod声明它。要使公共模块中的项也为公共,请在声明它们之前使用pub。
use关键字:在作用域中,use关键字创建项的快捷方式,以减少长路径的重复。在任何可以引用crate::garden::vegetables::Asparagus的作用域中,你可以使用crate::garden::vegetables::Asparagus创建一个快捷方式;从那时起,你只需要编写Asparagus就可以在作用域中使用该类型。
注意,
use
是用于减少名称重复的,pub mod
才是用于引入的.使用
mod
组织代码结构,提到src/main.rs和src/lib.rs被称为crate roots.命名的原因是这两个文件的内容在crate的模块结构(称为模块树)的根位置形成了一个名为crate的模块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
27mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
/// 模块树,注意
"""
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
"""Path
在模块树中如何找到一个item
绝对路径是从crate根目录开始的完整路径;对于来自外部crate的代码,绝对路径以crate名称开头,而对于来自当前crate的代码,它以文字crate开头。
相对路径从当前模块开始,使用当前模块中的self、super或标识符。
父模块中的项不能使用子模块中的私有项,但子模块中的项可以使用其祖先模块中的项。这是因为子模块封装并隐藏了它们的实现细节,但是子模块可以看到它们被定义的上下文。
sibling之间可以调用模块,比如同在crate下的函数和模块,这个函数可以直接调用模块而不使用pub.
相对路径
使用super,self等引入.
1 | fn deliver_order() {} |
注意使得模块中的enum,structs,函数等pub才能访问.
一个包可以同时包含src/main。Rs二进制crate根以及src/lib。默认情况下,两个crate都有包名。通常,具有这种既包含库又包含二进制crate模式的包在二进制crate中会有足够的代码来启动调用库crate中的代码的可执行文件。这使得其他项目可以从包提供的大部分功能中受益,因为库crate的代码可以共享
使用use
将paths引入作用域,要使用的话就要在同一mod作用域中.
1 | mod front_of_house { |
使用use将函数的父模块带入作用域意味着我们必须在调用函数时指定父模块。在调用函数时指定父模块可以清楚地表明该函数不是本地定义的,同时仍然可以最大限度地减少完整路径的重复。此外在使用struct、enum和其他项时,习惯上指定完整路径。
对于将两个具有相同名称的类型带入相同作用域的问题,除了引入父模块,还有另一种解决方案:在路径之后,可以为类型指定as和一个新的本地名称或别名。
1 | use std::fmt::Result; |
使用use关键字将名称引入作用域时,在新作用域中可用的名称是private。为了使调用代码的代码能够引用该名称,就好像它是在该代码的作用域中定义的一样,可以组合pub和use。这种技术被称为再导出
1 | // restaurant src/lib.rs |
在更改之前,外部代码必须通过restaurant::front_of_house::hosting::add_to_waitlist()来调用add_to_waitlist函数,这也需要将front_of_house模块标记为pub.现在外部代码可以使用restaurant::hosting::add_to_waitlist()代替。
标准std库也是一个包外部的crate。因为标准库是随Rust语言一起提供的,所以不需要更改Cargo.toml。但是需要用use来引用它,以便将其中的项引入包的作用域。
推荐模块命名不要为mod.rs
,使用名为mod.rs
文件的风格的主要缺点是项目最终可能会有许多名为mod.rs的文件,当同时在编辑器中打开它们时,这可能会令人困惑。
使用Cargo构建大项目
Release
在debug和release构建时可以使用不同的选项,在Cargo.toml
中
1 | [profile.dev] |
Cargo有两个主要配置profiles:运行Cargo构建时使用的开发配置文件和运行Cargo构建—发布时使用的发布配置文件。运行cargo build --release
使用release
profile.
当没有显式地添加任何profiles时,Cargo对应用的每个profiles都有默认设置。通过添加[profile.]*]部分,自定义的任何配置文件,覆盖任何子集的默认设置。
option-level设置控制Rust将应用于代码的优化数量,范围为0到3。应用更多的优化会延长编译时间,因此,如果您正在开发并经常编译代码,那么您可能希望通过更少的优化来加快编译速度,即使结果代码运行得更慢。
因此dev的默认选项级别为0。准备发布代码时,最好花更多的时间进行编译。将只在发布模式下编译一次,但是将多次运行编译后的程序,因此发布模式以较长的编译时间换取运行速度更快的代码
使用cargo publish
进行发布,注意需要注册crates.io账号,如果需要更新版本更改version
再推送即可
1 | [package] |
使用cargo yank --vers [versionNumber]
可以阻止使用这个项目的某个版本的依赖.
yank一个版本可以防止新项目依赖于该版本,同时允许所有依赖于该版本的现有项目继续进行。
workspaces
工作区是一组共享Cargo.lock,和输出目录的packages. 常见工作流是一个可执行文件的packages和多个生成库的packages.
在根目录下创建Cargo.toml
,其中添加packages名字,假设根目录名字add
1 | [workspace] |
1 | cargo new adder |
目录结构如下
1 | ├── Cargo.lock |
工作区在顶层有一个目标目录,编译后的工件将被放置到该目录中;adder包没有自己的目标目录。即使要从adder目录中运行cargo构建,编译后的工件仍然会在add/target而不是add/adder/target中结束。Cargo在工作区的目标目录中采用这样的结构,因为工作区的crate是相互依赖的。
如果每个crate都有自己的目标目录,那么每个crate都必须重新编译工作空间中的其他crate,以便将工件放置在自己的目标目录中。通过共享一个目标目录,crate可以避免不必要的重新构建。
1 | cargo new add_one --lib |
继续添加crate,加入workspace中.
1 | [workspace] |
添加本地项目中的依赖
1 | // adder/Cargo.toml |
在顶层目录运行cargo build
,然后指定-p
运行指定可执行程序cargo run -p adder
如果要使workspace下两个crate使用同一版本同一个依赖,可以使得其中一个crate依赖另一个依赖,build之后在顶层cargo.lock中会有相关依赖信息.
Cargo将确保工作空间中使用rand包的每个包中的每个crate都使用相同的版本,只要它们指定rand的兼容版本,就可以节省空间并确保工作空间中的crate彼此兼容
workspace中的项目测试,在顶层目录中cargo test
运行每个crate中的测试,使用cargo test -p
指定,发布项目同理,cargo publish -p
.
cargo install
命令在本地安装和使用二进制crate.
所有使用cargo install安装的二进制文件都存储在安装根目录的bin文件夹中。如果没有任何自定义配置,这个目录将是$HOME/.cargo/bin
如果$PATH
中的二进制文件名为cargo-something
,则可以通过运行Cargo something来将其作为Cargo子命令运行。在运行cargo——list时会列出这样的自定义命令。
cargo fmt/clippy
分别用于格式化和语法提示
cargo doc
非常方便的生成文档的工具
1 | cargo doc --no-deps --open |
test
单元测试 集成测试 benchmarks
1 | cargo test |
1 |
|
此外也有doc-test,用于测试文档中的代码.尝试使用criterion
Criterion.rs - Criterion.rs Documentation (bheisler.github.io)进行benchmark测试.
log
1 | use log::{error,warn,info,debug,trace}; |
dyn
Rust 编译器需要知道每个函数的返回类型需要多少空间。这意味着所有函数都必须返回一个具体类型。与其他语言不同,如果你有个像 Animal
那样的的 trait,则不能编写返回 Animal
的函数,因为其不同的实现将需要不同的内存量。
但是,有一个简单的解决方法。相比于直接返回一个 trait 对象,我们的函数返回一个包含一些 Animal
的 Box
。
1 | trait walk { |
box
只是对堆中某些内存的引用。因为引用的大小是静态已知的,并且编译器可以保证引用指向已分配的堆 Animal
,所以可以从函数中返回 trait.
每当在堆上分配内存时,Rust 都会尝试尽可能明确。因此,如果函数以这种方式返回指向堆的 trait 指针,则需要使用 dyn
关键字编写返回类型
1 | struct Sheep {} |
智能指针
与C++类似,rust也使用了一套机制保证内存安全. 智能指针相比于借用(引用)来说,其在大多数情况下拥有所有权.
常用智能指针类型如下
Box<T>
for allocating values on the heapRc<T>
, a reference counting type that enables multiple ownershipRef<T>
andRefMut<T>
, accessed throughRefCell<T>
, a type that enforces the borrowing rules at runtime instead of compile timeRefCell:智能指针,允许运行时动态获取可变引用,跟踪借用以保证安全性
- Arc:线程安全的reference counting type
此外,有Ref:用于在不可变借用的情况下安全地访问数据和Cell分别用来安全访问数据.
Box
1 | enum List { |
Rc
你必须通过使用Rust类型Rc\
当想要在堆上分配一些数据供程序的多个部分读取,并且在编译时无法确定哪个部分将最后使用该数据时,使用Rc\
注意,Rc\
1 | enum List { |
Rust中,Cell
是标准库中的一个类型,它提供了一种在可变引用的限制下安全地更新数据的方法。Cell
是一个非线程安全的类型,主要用于单线程环境下的可变状态管理。
1 | use std::cell::Cell; |
与Rc\
通过引用和Box\
在编译时检查借用规则的优点是,在开发过程中可以更快地捕获错误,并且不会对运行时性能产生影响,因为所有的分析都是事先完成的。由于这些原因,在大多数情况下,在编译时检查借用规则是最好的选择,这就是为什么这是Rust的默认值。
在运行时检查借阅规则的优点是,在编译时检查不允许的情况下,允许某些内存安全的场景。静态分析和Rust编译器一样,本质上是保守的。代码的一些属性是不可能通过分析代码来检测的
与Rc\
Arc
1 | use std::sync::{Arc, Mutex}; |
Weak
同c++中的weak_ptr,避免循环引用.
1 | use std::cell::RefCell; |
RefCell
Rc\
Box\
RefCell\
内部可变性是Rust中的一种设计模式,它允许你改变数据,即使数据有不可变的引用;
这就是使用RefCell的目的.
1 | pub trait Messenger { |
1 |
|
使用RefCell\
1 | // 在有多个owner情况下修改数据 使用Rc和RefCell |
总结一下,没有特别情况可以使用Box,类似于c++中unique_ptr,Rc类似于shared_ptr.
Arc和Mutex用于多线程情况. RefCell本身是运行时改变借用可变性,在一些情况下可以使用.
Trait 对比Concept in C++20
Rust中trait与泛型结合很好,同时由于Rust没有类的继承,可以考虑使用泛型继承和组合实现类似效果. 可以使用:
以及where和+
搭配可以对trait的继承以及对泛型的限制进行描述
1 | pub train walk { |
此外trait也可以写泛型(这在c++中往往是常见行为). trait在继承时使用:
和+
,在泛型使用时使用:
或where
,+
.
在c++中concept更偏向于限制泛型,而rust中trait还有接口的含义(通过实现接口而不是继承满足要求).
1 | template <typename T> |
声明concept如上,此外可以使用&&
搭配,还可以使用requires
1 | template <typename T> |
requires { requirement-seq }
requires ( parameter-list(optional) ) { requirement-seq }
requirements-seq
可以是:简单要求、类型要求、复合要求、嵌套要求.
1 | template <class T> |
而使用concept如下,使用和定义concept时都可以使用requires和&&.
1 | template <typename T> |
后记
作为偏底层的编程语言,c/c++,rust,zig等目前都还在发展,即使c++已过五十年,但C++2a中Concepts,Modules,Coroutines(协程)等新特性都不断出现(虽然在其他语言中早就有了),所以还是地位仍在的.而后两者在前端工具构建上均大显身手,期待后续发展.
我也很喜欢使用C/C++,Go,Rust等写一些小程序demo.
FYI
一些语言高级特性
- 本文链接: https://www.sekyoro.top/2024/06/29/Rust-learning-from-germ-to-grave/
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
欢迎关注我的其它发布渠道