Rust 学习笔记
Contents
安装
curl https://sh.rustup.rs -sSf | sh
成功后, 将下面代码加到 ~/.bash_profile
export PATH="$HOME/.cargo/bin:$PATH"
更新:
rustup update
卸载
rustup self uninstall
创建项目
可执行项目
cargo new project_name --bin
构建项目
cargo build
它会打包到 target/debug
构建并运行项目
cargo run
发布项目
cargo build --release
它会打包到 target/release
更新依赖包版本
cargo update
默认导入的包
crate
它是一个 Rust 代码的包.
可执行的项目, 称为 二进制 crate
, 库项目(即可以被其他程序调用的代码) 称为 库 crate
变量
在 rust 中, 变量默认的情况下是 不可变的 immutable
常量和不可变的变量
在 rust 中, 常量是不能用 mut
来修饰的, 它不光默认不能变, 它总是不能变的.
常量声明使用 const
而不是 let
, 并且必须注明值的类型.
常量只能用于常量表达式, 而不能作为函数调用的结果, 或任何其他只在运行时计算的值.
const MAX_POINTS: u32 = 100_000;
隐藏
- 除非再次使用
let
关键字, 否则默认情况下对变量重新赋值会导致编译错误 - 多次使用
let
时, 实际上创建了一个新的变量, 我们可以改变值的类型
, 从而复用这个名字. 但一个mut
的变量, 则不能修改它的类型, 否则会导致一个编译错误
数据类型
Rust 是一种静态类型语言, 也就是说在编译时就必须知道所有变量的类型.
- scalar : 标量
- 整型(有符号的, 以
i
开头, 无符号的以u
开头, 默认为i32
, 无论是在32位, 还是64位系统上, 默认都是i32
). 十六进制(0x开头), 八进制(0o开头), 二进制(0b开头), 字节(b'A'
) - 浮点型(
f32
,f64
, 默认为f64
, 因为在现代CPU中, 它与f32
速度几乎一样快) - 布尔类型(
bool
, 它只有true
或false
) - 字符类型(
char
, 它使用单引号指定. 不同与字符串使用双引号)
- 整型(有符号的, 以
- compound : 复合
- 元组(
tuple
), 它使用一个括号中的逗号分隔的值列表来创建一个元组.let tup: (i32, f64, u8) = (500, 6.4, 1);
, 这元素的类型是不必相同的.可以使用模式匹配来解构元组.let (x, y, z) = tup;
- 数组(
array
), 元素类型必须相同. 而且数组长度是固定的.索引从0开始.
- 元组(
函数
Rust 使用 snake case
风格来命名函数的. 即所有字母都是小写, 并使用下划线分隔单词.
Rust 可以调用定义过了的函数来调用(不管是在调用者之前还是之后定义, 都可以)
参数
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {}", x);
}
语句和表达式
语句: 执行一些操作但不返回值(如 let
语句)
表达式: 计算并产生一个值.它可以是语句的一部分. 例如函数调用, 宏调用.注意 {}
也是一个表达式.所以可以这样子:
let y = {
let x = 3;
x + 1
};
这里的{}
表达式的值为4.所以 y
为 4
返回值
函数的返回值, 等同于函数体最后一个表达式的值.
fn five() -> i32 {
5
}
注意, 如果是有分号的话, 则它是语句, 而不是表达式了.
if 表达式
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");
}
}
因为它是一个表达式, 所以它是有返回值的, 所以可以这样子:
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}
循环
loop
fn main() {
loop {
println!("again!");
}
}
while
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("LIFTOFF!!!");
}
for
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}
fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
rev()
用来反转.
所有权 ownership
一般程序管理内存的方式
- 使用GC
- 手工处理
而 Rust 则使用第三种: 内存被一个所有权系统管理, 它拥有一系列的规则使编译器在编译时进行检查.并且不会导致运行时开销.
所有权规则
- Rust 中每一个值都有一个称之为其
所有者 owner
的变量 - 值有且只能有一个所有者
- 当所有者(变量)离开作用域, 这个值将被丢弃.(当变量离开作用域时, Rust 为其调用一个特殊的函数, 这个函数叫
drop
)
之前提到的 数据类型
部分都是储存在 栈
上的并且离开作用域时被移出栈.
注意, Rust 永远也不会自动创建数据的 深拷贝
, 因此任何自动的复制可以被认为对运行时性能影响较小.
数据的交互方式
移动
let s1 = String::from("hello");
let s2 = s1;
克隆
String
有个 clone
函数, 它会深度复制堆上的数据.
let s1 = String::from("hello");
let s2 = s1.clone();
只在栈上的数据: 拷贝
let x = 5;
let y = x;
Rust 有一个 Copy
trait 的特殊注解, 可以用在类似整型这样的储存在栈上的类型. 如果一个类型拥有 Copy
trait, 一个旧的变量在将其赋值给其他变量后仍然可用.
Rust 不允许自身或任何部分实现了 Drop
trait 的类型使用 Copy
trait . 以下是一些 Copy
trait 的建议规则
- 所有整数类型
- 布尔类型
- 浮点类型
- 元组, 当且仅当其包含的类型也是
Copy
的时候.
引用与借用
&s1 语法允许我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
获取引用作为函数参数称为借用. 默认情况下, 引用也同样是不可变的.
可变引用
限制: 在特定作用域中的特定数据, 有且只有一个可变引用. 也不能在拥有不可变引用的同时拥有可变引用.
&mut
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
这使用 Rust 在编译时就避免了数据竞争.
悬垂指针
是其指向的内存可能已经被分配给其它持有者
在 Rust 中, 编译器确保引用永远也不会变成悬垂指针.
比如这代码在 Rust 中是编译不了的:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
解决的办法是返回 String 而不是 &String
. 返回 String 就表示所有权被转移出去了, 所以值还没有释放.
引用规则
- 在任意给定时间, 只能拥有以下中的一个
- 一个可变引用
- 任意数量的不可变引用
- 引用必须总是有效的
slices
这也是一个没有所有权的类型.
slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
字符串 slice
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
字符串字面值就是 slice, 即 let s = "Hello, world!";
的类型是 &str
, 这是一个不可变的引用.
结构体
定义
struct User {
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,
};
如果想改变 user1
的字段值, 则要声明为 let mut user1 = xxx
. Rust 并不允许只将特定字段标记为可变.
变量与字段同名时的初始化简写法
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
使用结构体更新语法从其他对象创建
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};
没有字段名的结构体
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);
方法
它的第一个参数总是 self
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
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
块中定义不以 self
作为参数的函数. 它们是函数, 而不是方法. 例如
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}
多个 impl 块
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
枚举
定义
enum IpAddrKind {
V4,
V6,
}
注意, 枚举也可以是这样子的:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
这与Java那种枚举不太一样..
使用
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
空值与 Option 枚举
标准库的定义
enum Option<T> {
Some(T),
None,
}
match 控制流运算符
遇到第一个符合时就会执行相应的代码.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
匹配 Option
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);
_
通配符
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
它会匹配所有的值.
if let
用来处理只匹配一个模式的值, 而忽略其他模式的情况.
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}
如果我们只想处理 Some(3)
时, 可以这样子简写
# let some_u8_value = Some(0u8);
if let Some(3) = some_u8_value {
println!("three");
}
模块
创建库模块
默认情况下 , cargo 就是创建库项目的, --bin
参数才是可执行项目.
cargo new lib_name
比较好的建议是:
在 src/lib.rs
文件中声明模块:
mod client;
mod network;
然后在其他的文件中, 提供模块的内容:
src/network.rs
的内容:
fn connect() {
}
mod server {
fn connect() {
}
}
注意, 这里不需要在 src/network.rs
文件中声明 mod network
了!
模块与文件系统的规则
- 如果一个叫
foo
的模块没有子模块, 应该将foo
的声明放入叫做foo.rs
的文件中 - 如果一个叫
foo
的模块有子模块, 应该将foo
的声明放入叫做foo/mod.rs
的文件中
私有性规则
- 如果一个项是公有的, 它能被任何父模块访问
- 如果一个项是私有的, 它能被其直接父模块及其子模块访问
使用 super 访问父模块
super::client::connect();
cargo 使用本地 crate
假设项目名为 rust-demo
,
项目目录结构
.
├── Cargo.lock
├── Cargo.toml
├── rust-demo.iml
├── src
│ ├── lib
│ │ ├── hello.rs
│ │ └── mod.rs
│ ├── main
│ └── main.rs
mod.rs
文件内容:
pub mod hello;
hello.rs
文件内容:
pub fn say_hello() {
println!("hello world")
}
main.rs
文件内容:
extern crate hello;
fn main() {
hello::say_hello()
}
Cargo.toml
文件内容
[package]
name = "rust-demo"
version = "0.1.0"
authors = ["emacsist <emacsist@qq.com>"]
[lib]
name = "hello"
path = "src/lib/hello.rs"
[[bin]]
name = "main"
path = "src/main.rs"
[dependencies]
rand = "0.3.14"
集合
vector
let v: Vec<i32> = Vec::new();
或
let v = vec![1, 2, 3];
更新
v.push(5);
获取
let third: &i32 = &v[2];
let third: Option<&i32> = v.get(2);
遍历
只读:
for i in &v {
println!("{}", i);
}
修改:
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
字符串
创建
let mut s = String::new();
let s = String::from("initial contents");
更新
let mut s = String::from("foo");
s.push_str("bar");
拼接
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
或
let s = format!("{}-{}-{}", s1, s2, s3);
不支持索引字符串, 如 s[0]
字符串 slice
let hello = "Здравствуйте";
let s = &hello[0..4];
遍历
for c in "नमस्ते".chars() {
println!("{}", c);
}
hash map
创建
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
访问
use std::collections::HashMap;
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);
更新
插入一个相同的 key 名时, 会覆盖前一个值
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
只在键没有数据时插入
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
根据旧值更新一个值
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
错误处理
可恢复错误: Result<T, E>
不可恢复错误: panic!
默认下, panic!
宏会 unwinding 程序, 这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据. 另一种选择是终止 abort
(这会使二进制文件更小). 可以在 Cargo.toml
文件中配置:
[profile.release]
panic = 'abort'
fn main() {
panic!("crash and burn");
}
简写 unwrap 和 expect
unwrap
, 如果 Result 是 Ok, 则返回 Ok 中的值, 如果是 Err, 则会调用 panic!
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
expect
, 类似 unwrap
, 但提供一个好的错误信息.
传播错误
返回 Result<T, E>
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
简写 ?
问号运算符
向调用者返回错误的函数
并且要注意, 它只能被用于返回 Result 的函数
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
泛型
泛型的函数签名:
fn largest<T>(list: &[T]) -> T {
结构体中的泛型:
struct Point<T, U> {
x: T,
y: U,
}
trait
定义共享的行为
定义
pub trait Summarizable {
fn summary(&self) -> String;
}
一个 trait 可以有多个方法签名定义.
实现
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summarizable for NewsArticle {
fn summary(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
默认实现
pub trait Summarizable {
fn summary(&self) -> String {
String::from("(Read more...)")
}
}
impl Summarizable for NewsArticle {}
trait bounds
pub fn notify<T: Summarizable>(item: T) {
println!("Breaking news! {}", item.summary());
}
生命周期
单个的生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系
注解法 : 'a
, 这表示 有一个名为 'a
的生命周期参数.
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域
结构体的生命周期:
struct ImportantExcerpt<'a> {
part: &'a str,
}
何时不需要生命周期注解的规则
- 每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:
fn foo<'a>(x: &'a i32)
,有两个引用参数的函数有两个不同的生命周期参数,fn foo<‘a, ‘b>(x: &‘a i32, y: &‘b i32),依此类推 - 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:
fn foo<'a>(x: &'a i32) -> &'a i32
- 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为
&self
或&mut self
,那么self
的生命周期被赋给所有输出生命周期参数。这使得方法编写起来更简洁。
测试
在 fn
行之前加上 #[test]
就会将当前的函数变成测试函数.
执行 test
cargo test
assert!
宏检测 bool 结果
assert!(larger.can_hold(&smaller));
相等或不相等测试
assert_eq!(4, add_two(2));
assert_ne!(4, add_two(2));
并行或串行测试
默认会使用线程来并行的运行测试. 也可以指定线程数:
cargo test -- --test-threads=1
测试通过时也打印输出
cargo test -- --nocapture
单元测试
通过测试函数名来进行测试
cargo test one_hundred
注意, 这种情况下, 默认会执行所有以 one_hundred
名字开头的函数的.
忽略测试
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
只运行那些忽略的测试函数
cargo test -- --ignored
组织结构
单元测试
传统做法是在每个文件中创建包含测试函数的 tests
模块,并使用 cfg(test)
标注模块。
#[cfg(test)]
告诉 Rust 只在执行 cargo test 时才编译和运行测试代码,而在运行 cargo build 时不这么做.
集成测试
在 src
目录下创建一个 tests
目录.
tests/integration_test.rs
:
extern crate adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
命令行程序
获取参数
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
let query = &args[1];
let filename = &args[2];
}
注意. 第0个参数是程序路径和程序名.
读取和输出文件内容
let mut f = File::open(filename).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("something went wrong reading the file");
println!("With text:\n{}", contents);
闭包: 可以捕获环境的匿名函数
可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值.
闭包类型推断和注解
闭包不要求像 fn 函数那样在参数和返回值上注明类型.
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
当然, 你也可以显式注明:
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
多次调用闭包注意
可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 memoization 或 lazy evaluation。
可以使用带有 Fn
系列 trait 由标准库提供的闭包.这些都实现了 trait Fn
, FnMut
, FnOnce
中的一个.
迭代器
它是惰性的.
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
消费迭代器
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
产生其他迭代器
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
Cargo
发布配置
默认情况下下, 使用的是 dev
配置.即 (cargo build) 时, 使用的是 dev
配置.
也可以使用 release
:
cargo build --release
Cargo.toml
中默认的配置:
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
工作空间
$ mkdir add
$ cd add
$ vim Cargo.toml
[workspace]
members = [
"adder",
]
然后在 add 目录下执行命令创建一个二进制 crate. 然后在 add
中执行命令: cargo build
cargo new --bin adder
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
可以这样子创建多个 crate.
然后编译指定的 project:
cargo build -p adder
安装依赖
cargo install ripgrep
智能指针
在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反大部分情况,智能指针 拥有 他们指向的数据。 智能指针通常使用结构体实现.
Box : 在堆上存储数据,并且可确定大小
Deref trait : 将智能指针当作常规引用处理
Drop trait: 运行清理代码
Rc: 引用计数智能指针
并发
线程
创建线程
thread::spawn
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
join 等待线程结束
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
move 闭包
强制闭包获取其使用的环境值的所有权
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
消息传递
创建通道
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
# tx.send(()).unwrap();
}
mpsc 是 多个生产者,单个消费者
生产者和消费者:
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
}
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {}", received);
}
}
共享状态
互斥器: Mutex<T>