本文最后更新于 2024-02-26T11:50:11+00:00
rust
参考文献
第一章-rust环境
安装rust
windows
在官网 https://www.rust-lang.org/ 下载,即可自动安装
linux/Mac
1 curl https://sh.rustup.rs -sSf | sh
更新rust
卸载rust
查看rust版本
使用本地浏览器离线查看rust文档
安装和切换特定版本的rust
例如安装2024-01-31nightly版本的rustc
1 rustup install nightly-2024-01-31
查看安装好的版本列表
切换默认rust版本
删除指定rust版本
1 rustup toolchain uninstall 版本全名
查看当前rustc版本
hello_world
文件命名规范:小写字母,下划线作空格,后缀名.rs
1 2 3 fn main (){ println! ("Hello world!" ); }
编译
运行(windows)
Cargo
Cargo 是 Rust的构建系统和包管理工具
构建代码、下载依赖的库、构建这些库…
安装 Rust 的时候会安装 Cargo,检查cargo的版本
使用cargo创建项目
项目名称是 hello_cargo
会创建一个新的目录 hello_cargo
Cargo.toml
src 目录
初始化了一个新的Git仓库, .gitignore
Cargo.toml
TOML (Tom’s Obvious, Minimal Language)格式,是Cargo的配置格式
[pacakge],是一个区域标题,表示下方内容是用来配置包(package)的
1 2 3 4 5 6 7 8 9 10 [package] name = "hello_cargo" version = "0.1.0" edition = "2021" authors = ["cjl <alittletest01@outlook.com>" ] [dependencies]
[dependencies],另一个区域的开始
它会列出项目的依赖项。在 Rust 里面,代码的包称作 crate。
cargo 生成的 main.rs 在 src 目录下
而Cargo.toml在项目顶层下
源代码都应该在 src 目录下
顶层目录可以放置: README、许可信息、配置文件和其它与程序源码无关的文件
如果创建项目时没有使用cargo,也可以把项目转化为使用cargo:
把源代码文件移动到 src 下
创建 Cargo.toml 并填写相应的配置
构建Cargo项目
会在target/debug/ 生成可执行文件
第一次运行 cargo build 会在顶层目录生成 cargo.lock 文件
该文件负责追踪项目依赖的精确版本
不需要手动修改该文件
构建并运行cargo项目
检查代码 ,确保能通过编译,但是不产生任何可执行文件
cargo check 要比 cargo build 快得多
编写代码的时候可以连续反复的使用cargo check检查代码,提高效率
为发布构建
编译时会进行优化代码
会运行的更快,但是编译时间更长
会在 target/release 而不是 target/debug 生成可执行文件
第二章-初识rust
你会在本章看到一个猜数程序的例子,感受rust语言的特点
其中具体的语法会在第三章开始逐一详细解释
一次猜测
1 2 3 4 5 6 7 8 9 10 11 12 use std::io;fn main () { println! ("猜数!" ); println! ("猜测一个数" ); let mut foo =1 ; foo=2 ; let bar =foo; let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("无法读取行" ); println! ("你猜测的数是:{}" ,guess); }
可以在https://crates.io/crates 查看rust的库
添加rand库
在Cargo.toml修改以下内容
1 2 [dependencies] rand = "0.3.14"
生成神秘数字并猜测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 use std::{cmp::Ordering, io};use rand::Rng;fn main () { println! ("猜数!" ); let secret_number = rand::thread_rng ().gen_range (1 , 101 ); println! ("神秘数字是:{}" ,secret_number); println! ("猜测一个数" ); let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("无法读取行" ); let guess : u32 = guess.trim ().parse ().expect ("Please type a number" ); println! ("您猜测的数是:{}" ,guess); match guess.cmp (&secret_number){ Ordering::Less => println! ("Too small!" ), Ordering::Greater => println! ("Too big" ), Ordering::Equal => println! ("You win" ), } }
允许多次猜测
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 27 28 29 30 31 use std::{cmp::Ordering, io};use rand::Rng;fn main () { println! ("猜数!" ); let secret_number = rand::thread_rng ().gen_range (1 , 101 ); loop { println! ("猜测一个数" ); let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("无法读取行" ); let guess : u32 = match guess.trim ().parse (){ Ok (num)=>num, Err (_)=>continue , }; println! ("您猜测的数是:{}" ,guess); match guess.cmp (&secret_number){ Ordering::Less => println! ("Too small!" ), Ordering::Greater => println! ("Too big" ), Ordering::Equal => { println! ("You win!" ); break ; }, } } }
第三章-通用的编程概念
变量与可变性
变量
声明变量使用 let 关键字
默认情况下,变量是不可变的(Immutable)
声明变量时,在变量前面加上 mut,就可以使变量可变。
常量
常量(constant) ,常量在绑定值以后也是不可变的,但是它与不可变的变量有很多区别:
不可以使用 mut,常量永远都是不可变的
声明常量使用 const关键字,它的类型必须被标注
常量可以在任何作用域内进行声明,包括全局作用域
常量只可以绑定到常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算出的值
在程序运行期间,常量在其声明的作用域内一直有效
命名规范:Rust 里常量使用全大写字母,每个单词之间用下划线分开,例如:
MAX_POINTS
例子:
1 const MAX_POINTS : u32 = 100_000
隐藏(shadowing)
可以使用相同的名字声明新的变量,新的变量就会shadow (隐藏)之前声明的同名变量
shadow 和把变量标记为 mut 是不一样的:
如果不使用let关键字,那么重新给非mut的变量赋值会导致编译时错误
而使用let声明的同名新变量,也是不可变的
使用 let 声明的同名新变量,它的类型可以与之前不同
示例代码:
1 2 3 4 5 6 fn main () { let x = 5 ; let x = x + 1 ; let x = x * 2 ; println! ("The value of x is {}" , x); }
小提示: 使用shift+alt+F
可以格式化代码
1 2 3 4 5 fn main () { let spaces = " " ; let spaces = spaces.len (); println! ("{}" , spaces); }
数据类型:标量类型
Rust是静态编译语言,在编译时必须知道所有变量的类型
基于使用的值,编译器通常能够推断出它的具体类型
但如果可能的类型比较多(例如把String转为整数的parse方法) ,就必须添加类型的标注,否则编译会报错
例如:
1 2 3 4 fn main () { let guess : i32 = "42" .parse ().expect ("not a number" ); }
小提示: 使用shift+alt+↑
或shift+alt+↓
可以复制当前行的代码
一个标量类型代表一个单个的值
Rust 有四个主要的标量类型:
整数类型
arch是architecture的缩写,isize和usize的位数由程序所运行的计算机架构决定,使用 isize 或 usize 的主要场景是对某种集合进行索引操作。
整数字面值
除了byte类型外,所有的数值字面值都允许使用类型后缀,例如57u8
如果你不太清楚应该使用那种类型,可以使用Rust相应的默认类型:
整数的默认类型就是 i32
: 总体上来说速度很快,即使在64位系统中
整数溢出
例如:U8 的范围是 0-255,如果你把一个 U8 变量的值设为 256,那么:
浮点类型
Rust有两种基础的浮点类型,也就是含有小数部分的类型
Rust的浮点类型使用了IEEE-754标准来表述,f64是默认类型, 因为在现代CPU上f64和f32的速度差不多,而且精度更高。
rust的加减乘除取余操作和其他语言是一样的
布尔类型
Rust的布尔类型也有两个值: true和false
一个字节大小
符号是 bool
字符类型
Rust 语言中 char 类型被用来描述语言中最基础的单个字符。
字符类型的字面值使用单引号
占用4字节大小
是Unicode标量值,可以表示比ASCIl 多得多的字符内容:拼音、中日韩文、零长度空白字符、emoji 表情等。
U+0000 到 U+D7FF
U+E000 到 U+10FFFF
但Unicode中并没有“字符”的概念,所以直觉上认为的字符也许与Rust中的概念并不相符
1 2 3 4 5 fn main () { let x = 'z' ; let y = '字' ; let z = '🦀'; }
数据类型:复合类型
tuple
用小括号包围, 逗号分隔, 长度固定, 和python的元组相同
1 2 3 4 5 6 7 8 9 fn main () { let tup : (i32 , f64 , u8 ) = (500 , 6.4 , 1 ); println! ("{},{},{}" , tup.0 , tup.1 , tup.2 ); let (x, y, z) = tup; println! ("{},{},{}" , x, y, z); }
数组
数组也可以将多个值放在一个类型里
数组中每个元素的类型必须相同
数组的长度也是固定的
数组的用处
如果想让你的数据存放在stack (栈)上而不是heap (堆)上,或者想保证有固定数量的元素,这时使用数组更有好处
数组没有Vector灵活(以后再讲)。
Vector 和数组类似,它由标准库提供
Vector 的长度可以改变
如果你不确定应该用数组还是 Vector,那么估计你应该用 Vector。
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let a1 = [1 , 2 , 3 , 4 , 5 ]; let a2 = [1 ; 5 ]; println! ("{}" , a1[1 ]); let b = a2[a1[4 ]]; println! ("{}" , b); let c = [1 , 2 , 3 ]; }
可以看出,rust在编译的时候只会对数组越界作简单的检查
函数和注释
1 2 3 4 5 6 7 8 9 10 fn main () { let a = test01 (4 ); println! ("{}" , a); }fn test01 (x: i32 ) -> i32 { println! ("this is function" ); x + 5 }
注释的方法有//
和/* xxx */
更多注释写法
if-else
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let num = 6 ; if num % 4 == 0 { println! ("number is divisible by 4" ); } else if num % 3 == 0 { println! ("number is divisible by 3" ); } else if num % 2 == 0 { println! ("number is divisible by 2" ); } else { println! ("number is not divisible by 4,3,2" ); } }
值得注意的是,if语句只允许接收bool语句作为条件, if 1
是不合法的
当if-else较多时,建议使用match重构代码
1 2 3 4 5 6 fn main () { let condition = true ; let a = if condition { 5 } else { 6 }; println! ("{}" , a); }
循环
loop
1 2 3 4 5 6 7 8 9 fn main () { let mut counter = 0 ; let result = loop { counter += 1 ; if counter == 10 { break counter * 2 ; } }; }
while
1 2 3 4 5 6 7 8 fn main () { let mut num =3 ; while num !=0 { println! ("{}" ,num); num-=1 ; } println! ("LIFTOFF!" ); }
for
1 2 3 4 5 6 7 fn main () { let arr = [1 , 2 , 4 , 8 ]; for a in arr.iter () { println! ("{}" , a); } }
1 2 3 4 5 6 fn main () { for num in (1 ..4 ).rev () { println! ("{}" , num); } println! ("LIFTOFF!" ); }
第四章-所有权
Rust 的核心特性就是所有权
所有程序在运行时都必须管理它们使用计算机内存的方式
有些语言有垃圾收集机制,在程序运行时,它们会不断地寻找不再使用的内存
在其他语言中,程序员必须显式地分配和释放内存
Rust 采用了第三种方式:
内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则。
当程序运行时,所有权特性不会减慢程序的运行速度。
Stack vs heap
计组的内容,这里略
所有权解决的问题:
跟踪代码的哪些部分正在使用 heap 的哪些数据
最小化heap上的重复数据量
清理 heap上未使用的数据以避免空间不足。
一旦你懂的了所有权,那么就不需要经常去想 stack 或 heap 了。
但是知道管理 heap数据是所有权存在的原因,这有助于解释它为什么会这样工作。
所有权规则,内存与分配
所有权规则
每个值都有一个变量,这个变量是该值的所有者
每个值同时只能有一个所有者
当所有者超出作用域(scope)时,该值将被删除。
内存与分配
1 2 3 4 5 fn main () { let mut s = String ::from ("hello" ); s.push_str (",world" ); println! ("{}" , s); }
字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里
String类型,为了支持可变性,需要在heap上分配内存来保存编译时未知的文本内容:
操作系统必须在运行时来请求内存,这步通过调用 String::from
来实现
当用完 String 之后,需要使用某种方式将内存返回给操作系统
这步,在拥有 GC的语言中,GC会跟踪并清理不再使用的内存
没有GC,就需要我们去识别内存何时不再使用,并调用代码将它返回。
如果忘了,那就浪费内存。
如果提前做了,变量就会非法
如果做了两次,也是Bug。必须一次分配对应一次释放
Rust采用了不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交还给操作系统。(例子)
string的移动
一个String 由3部分组成:
一个指向存放字符串内容的内存的指针
一个长度
一个容量
上面这些东西放在stack上。存放字符串内容的部分在heap上
长度len,就是存放字符串内容所需的字节数
容量 capacity是指String从操作系统总共申请的内存
当执行下面的代码以移动string 时
为了保证内存安全:
Rust没有尝试复制被分配的内存
Rust 让 s1 失效。当 s1 离开作用域的时候, Rust 不需要释放任何东西
你也许会将复制指针、长度、容量视为浅拷贝,但由于Rust让s1失效了,所以我们用一个新的术语:移动(Move)
隐含的一个设计原则: Rust不会自动创建数据的深拷贝, 就运行时性能而言,任何自动赋值的操作都是廉价的
string的克隆
1 2 3 4 5 fn main () { let s1 = String ::from ("hello" ); let s2 = s1.clone (); println! ("{},{}" , s1, s2); }
拥有Copy trait的类型
任何简单标量的组合类型都可以是 Copy的
任何需要分配内存或某种资源的都不是 Copy 的
一些拥有 Copy trait 的类型:
所有的整数类型,例如 u32
bool
char
所有的浮点类型,例如 f64
Tuple (元组) ,如果其所有的字段都是Copy的
(i32, String) 不是
(i32, i32) 是
所有权和函数
函数在传入值和返回值的过程中都会发生所有权的转移
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let s1 = gives_ownership (); let s2 = String ::from ("hello" ); let s3 = takes_and_gives_back (s2); }fn gives_ownership () -> String { let some_string = String ::from ("hello" ); some_string }fn takes_and_gives_back (a_string: String ) -> String { a_string }
引用和借用
&符号就表示引用:允许你引用某些值而不取得其所有权
引用默认是不可变的, 可变引用可以修改引用的数据,写作&mut
1 2 3 4 5 6 7 8 9 10 fn main () { let mut s1 = String ::from ("hello" ); let len = calculate_length (&mut s1); println! ("The length of {} is {}" , s1, len); }fn calculate_length (s: &mut String ) -> usize { s.push_str (",world!" ); s.len () }
1 The length of hello,world! is 12
可变引用有一个重要的限制:在特定作用域内,对某一块数据,只能有一个可变的引用。这样做的好处是可在编译时防止数据竞争。
在任意给定时间,要么只能有一个可变引用,要么只能有任意数量的不可变引用。
1 2 3 4 5 6 fn main () { let mut s = String ::from ("hello" ); let r1 = &s; let r2 = &mut s; println! ("{}, {}" , r1, r2); }
以下三种行为下会发生数据竞争:
两个或多个指针同时访问同一个数据
至少有一个指针用于写入数据
没有使用任何机制来同步对数据的访问
可以通过创建新的作用域,来允许非同时的创建多个可变引用
1 2 3 4 5 6 7 fn main () { let mut s = String ::from ("hello" ); { let s1 = &mut s; } let s2 = &mut s; }
悬空引用: Dangling References
在 Rust 里,编译器可保证引用永远都不是悬空引用:如果你引用了某些数据,编译器将保证在引用离开作用域之前数据不会离开作用域,否则会报错
1 2 3 4 5 6 7 8 fn main () { let r = dangle (); }fn dangle () -> &String { let s = String ::from ("hello" ); &s }
切片
切片(slice),是另外一种不持有所有权的数据类型
字符串切片的使用
1 2 3 4 5 6 7 8 9 10 11 fn main () { let s = String ::from ("hello world" ); let hello = &s[..5 ]; let world = &s[6 ..]; let whole = &s[..]; let llo = &s[2 ..5 ]; println! ("{}" , hello); println! ("{}" , world); println! ("{}" , whole); println! ("{}" , llo); }
定义一个函数,实现找到字符串的第一个单词
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fn main () { let mut s = String ::from ("hello world" ); let word = first_word (&s); s.clear (); println! ("{}" , word); }fn first_word (s: &String ) -> &str { let bytes = s.as_bytes (); for (i, &item) in bytes.iter ().enumerate () { if item == b' ' { return &s[..i]; } } &s[..] }
定义函数时使用字符串切片来代替字符串引用会使我们的API更加通用,且不会损失任何功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fn main () { let mut my_string = String ::from ("hello world" ); let wordIndex = first_word (&my_string[..]); let my_string_literal = "hello world" ; let wordIndex = first_word (my_string_literal); }fn first_word (s: &str ) -> &str { let bytes = s.as_bytes (); for (i, &item) in bytes.iter ().enumerate () { if item == b' ' { return &s[..i]; } } &s[..] }
第五章-struct
定义和实例化struct
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 27 28 struct User { username: String , email: String , sign_in_count: u64 , active: bool , }struct Color (i32 , i32 , i32 );fn main () { let user1 = User { email: String ::from ("cjl@algorithmpark.xyz" ), username: String ::from ("cjl" ), active: true , sign_in_count: 114514 , }; println! ("{}" , user1.active); let user2 = User { email: String ::from ("root@algorithmpark.xyz" ), username: String ::from ("root" ), ..user1 }; let black = Color (0 , 0 , 0 ); }
如果字段使用了String而不是&str:
该 struct 实例拥有其所有的数据
只要 struct 实例是有效的,那么里面的字段数据也是有效的
struct里也可以存放引用,但这需要使用生命周期(以后讲)。
生命周期保证只要 struct实例是有效的,那么里面的引用也是有效的。
如果 struct 里面存储引用,而不使用生命周期,就会报错(例子)。
结构体案例:长方形
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #[derive(Debug)] struct Rectangle { width: u32 , length: u32 , }fn main () { let rect = Rectangle { width: 30 , length: 50 , }; println! ("{}" , area (&rect)); println! ("{:#?}" , rect); }fn area (rect: &Rectangle) -> u32 { rect.width * rect.length }
1 2 3 4 5 1500 Rectangle { width: 30 , length: 50 , }
结构体方法
使用impl块定义方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #[derive(Debug)] struct Rectangle { width: u32 , length: u32 , }impl Rectangle { fn area (&self ) -> u32 { self .length * self .width } }fn main () { let rect = Rectangle { width: 30 , length: 50 , }; println! ("{}" , rect.area ()); println! ("{:#?}" , rect); }
1 2 3 4 5 1500 Rectangle { width: 30 , length: 50 , }
如果不传入self,可以实现构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #[derive(Debug)] struct Rectangle { width: u32 , length: u32 , }impl Rectangle { fn square (size: u32 ) -> Rectangle { Rectangle { width: size, length: size, } } }fn main () { let rect = Rectangle::square (5 ); println! ("{:#?}" , rect); }
第六章-枚举类型
定义枚举
1 2 3 4 5 6 7 8 9 10 11 12 13 enum IpAddrKind { V4, V6, }fn main () { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route (four); route (six); route (IpAddrKind::V4); }fn route (ip_kind: IpAddrKind) {}
将数据附加到枚举的变体中
1 2 3 4 5 6 7 8 enum IpAddrKind { V4 (u8 ,u8 ,u8 ,u8 ), V6 (String ), }fn main (){ let home = IpAddrKind::V4 (127 , 0 , 0 , 1 ); let loopback = IpAddrKind::V6 (String ::from ("::1" )); }
更加复杂的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 enum Message { Quit, Move { x: i32 , y: i32 }, Write (String ), ChangeColor (i32 , i32 , i32 ), }impl Message { fn call (&self ) {} }fn main () { let q = Message::Quit; let m = Message::Move { x: 12 , y: 24 }; let w = Message::Write (String ::from ("Hello" )); let c = Message::ChangeColor (0 , 255 , 255 ); m.call (); }
option枚举
标准库中的定义
1 2 3 4 enum Option <T>{ Some (T), None , }
它包含在Prelude(预导入模块)中,可以直接使用
1 2 3 4 5 fn main (){ let x :i8 =5 ; let y :Option <i8 > =Some (5 ); let sum =x+y; }
Option的作用是作为null,因为它的类型无法和正常的类型计算,它可以避免和null作计算
match
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 enum Coin { Penny, Nickel, Dime, Quarter, }fn value_in_cents (coin: Coin) -> u8 { match coin { Coin::Penny => { println! ("Penny!" ); 1 } Coin::Nickel => 5 , Coin::Dime => 10 , Coin::Quarter => 25 , } }fn main () { println! ("{}" , value_in_cents (Coin::Penny)); }
绑定值的模式匹配
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 27 28 29 #[derive(Debug)] enum UsState { Alabama, Alaska, }enum Coin { Penny, Nickel, Dime, Quarter (UsState), }fn value_in_cents (coin: Coin) -> u8 { match coin { Coin::Penny => { println! ("Penny!" ); 1 } Coin::Nickel => 5 , Coin::Dime => 10 , Coin::Quarter (state) => { println! ("State quarter from {:?}!" , state); 25 } } }fn main () { let c = Coin::Quarter (UsState::Alaska); println! ("{}" , value_in_cents (c)); }
1 2 State quarter from Alaska!25
match 要求枚举所有的可能, 如果没有全部枚举, 会报错
可以使用_
表示通配剩余的情况
1 2 3 4 5 6 7 8 9 10 fn main () { let v = 0u8 ; match v { 1 => println! ("one" ), 3 => println! ("three" ), 5 => println! ("five" ), 7 => println! ("seven" ), _ => (), } }
if let
if let 可以作为一种特殊的match的简写
1 2 3 4 5 6 7 8 9 10 11 fn main () { let v = Some (0u8 ); match v { Some (3 ) => println! ("three" ), _ => (), } if let Some (3 ) = v { println! ("three" ); } }
if let可以搭配else
1 2 3 4 5 6 7 8 9 10 11 12 13 fn main () { let v = Some (0u8 ); match v { Some (3 ) => println! ("three" ), _ => println! ("others" ), } if let Some (3 ) = v { println! ("three" ); } else { println! ("others" ); } }
第七章-模块系统
package,Crate,Module
代码组织主要包括:
哪些细节可以暴露,哪些细节是私有的
作用域内哪些名称有效
模块系统
Package(包):Cargo 的特性,让你构建、测试、共享 crate
Crate (单元包):一个模块树,它可产生一个 library 或可执行文件
Module (模块)、use:让你控制代码的组织、作用域、私有路径
Path (路径) :为struct、function或 module等项命名的方式
Crate 的类型:
Crate Root:
是源代码文件
Rust 编译器从这里开始,组成你的 Crate 的根 Module
一个 Package :
包含1个Cargo.toml,它描述了如何构建这些 Crates
只能包含 0-1 个 library crate
可以包含任意数量的 binary crate
但必须至少包含一个 crate (library 或 binary)
Cargo的惯例
src/main.rs:
binary crate的 crate root
crate 名与 package 名相同
src/lib.rs:
crate 名与 package 名相同
library crate 的 crate root
package包含一个 library crate
Cargo把 crate root 文件交给 rustc 来构建library或 binary
一个 Package 可以同时包含 src/main.rs 和 src/lib.rs
一个 binary crate,一个library crate
名称与package名相同
一个Package可以有多个binary crate:
文件放在 src/bin
每个文件是单独的 binary crate
Carte将相关功能组合到一个作用域内,便于在项目间进行共享, 防止冲突
定义module来控制作用域和私有性
Module:
在一个crate 内,将代码进行分组
增加可读性,易于复用
控制项目(item)的私有性。public、 private
建立 module
mod 关键字
可嵌套
可包含其它项(struct、enum、常量、trait、函数等)的定义
在lib.rs创建模块
1 2 3 4 5 6 7 8 9 10 11 mod front_of_house { mod hosting { fn add_to_waitlist () {} fn seat_at_table () {} } mod serving { fn take_order () {} fn serve_order () {} fn take_payment () {} } }
path路径
私有边界(privacy boundary)
模块不仅可以组织代码,还可以定义私有边界。
如果想把函数或 struct等设为私有,可以将它放到某个模块中。
Rust中所有的条目(函数,方法, struct, enum,模块,常量)默认是私有的。
父级模块无法访问子模块中的私有条目
子模块里可以使用所有祖先模块中的条目
使用 pub 关键字来将某些条目标记为公共的
1 2 3 4 5 6 7 8 9 10 11 mod front_of_house { pub mod hosting { pub fn add_to_waitlist () {} } }pub fn eat_at_restaurant () { crate::front_of_house::hosting::add_to_waitlist (); front_of_house::hosting::add_to_waitlist (); }
super
1 2 3 4 5 6 7 8 9 10 11 12 fn serve_order () {}mod back_of_house { fn fix_incorrect_order () { cook_order (); super::serve_order (); crate::serve_order (); } fn cook_order () {} }
pub struct
pub 放在 struct 前:
struct 是公共的
struct 的字段默认是私有的
struct 的字段需要单独设置 pub 来变成共有。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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), 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 enum
pub 放在 enum 前:
use关键字
可以使用 use关键字将路径导入到作用域内
使用use引入自定义函数
1 2 3 4 5 6 7 8 9 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 (); }
使用use引入标准库的struct
1 2 3 4 5 use std::collections::HashMap;fn main () { let mut map = HashMap::new (); map.insert (1 , 2 ); }
使用as可以添加本地别名
使用 pub use 重新导出名称
使用use将路径(名称)导入到作用域内后,该名称在此作用域内是私有 的。
pub use:重导出
将条目引入作用域
该条目可以被外部代码引入到它们的作用域
使用外部包(package)
Cargo.toml添加依赖的包(package)
use将特定条目引入作用域
嵌套路径以简写use语句
1 2 use std::{cmp::Ordering, io};use std::io::{self ,Write};
通配符*
1 use std::collections::*;
给cargo换源
在~\.cargo\
打开命令行
添加以下内容
1 2 3 4 5 6 7 8 9 [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" [net] git-fetch-with-cli = true
将模块内容放到其他文件
模块定义时,如果模块名后边是;
,而不是代码块:
Rust 会从与模块同名的文件中加载内容
模块树的结构不会变化
随着模块逐渐变大,该技术让你可以把模块的内容移动到其它文件中
例如,在lib.rs中有个模块mod front_of_house
1 2 3 4 5 mod front_of_house{ pub mod hosting{ pub fn add_to_waitlist (){} } }
可以把上述的代码变为
然后再src目录下新建文件front_of_house.rs
, 其中内容为
1 2 3 pub mod hosting{ pub fn add_to_waitlist (){} }
第八章-常用的集合
vector
构造vector
1 2 3 4 5 6 fn main () { let v1 : Vec <i32 > = Vec ::new (); let v2 = vec! [1 , 2 , 3 ]; }
添加元素
1 2 3 4 5 6 7 8 9 fn main () { let mut v = Vec ::new (); v.push (1 ); v.push (2 ); v.push (3 ); v.push (4 ); }
访问元素
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let v = vec! [1 , 2 , 3 , 4 , 5 ]; let third : &i32 = &v[2 ]; println! ("The third element is {}" , third); match v.get (2 ) { Some (third) => println! ("The third element is {}" , third), None => println! ("There is no third element" ), } }
1 2 The third element is 3 The third element is 3
1 2 3 4 5 6 7 8 fn main () { let mut v = vec! [1 , 2 , 3 , 4 , 5 ]; let first = &v[0 ]; v.push (6 ); println! ("{}" , first); }
会出现报错:cannot borrow v
as mutable because it is also borrowed as immutable
first首先借用了不可变的引用,随后push操作借用了可变的引用,这样是不安全的,因为push操作过后v
存放的内存地址可能会改变,导致first借用的引用失效
遍历和修改
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let mut v = vec! [1 , 2 , 3 , 4 , 5 ]; let first = &mut v[0 ]; *first = 114514 ; for i in &v { println! ("{}" , i); } }
1 2 3 4 5 6 7 fn main () { let mut v = vec! [1 , 2 , 3 , 4 , 5 ]; for i in &mut v { *i += 50 ; println! ("{}" , *i); } }
配合enum实现存放多种类型数据
1 2 3 4 5 6 7 8 9 10 11 12 enum SpreadsheetCell { Int (i32 ), Float (f64 ), Text (String ), }fn main () { let row = vec! [ SpreadsheetCell::Int (3 ), SpreadsheetCell::Text (String ::from ("blue" )), SpreadsheetCell::Float (114.514 ), ]; }
String
来自 标准库 而不是 核心语言
可增长、可修改、可拥有
UTF-8 编码
构造
构造空字符串
1 2 3 fn main (){ let s =String ::new (); }
构造字符串并初始化
1 2 3 4 5 fn main () { let s1 = String ::from ("abc" ); let s2 = "def" .to_string (); println! ("{},{}" , s1, s2); }
String支持多语言
1 2 3 4 5 6 7 8 9 10 11 fn main () { let hello = String ::from ("Dobrý den" ); let hello = String ::from ("Hello" ); let hello = String ::from ("नमस्ते" ); let hello = String ::from ("こんにちは" ); let hello = String ::from ("안녕하세요" ); let hello = String ::from ("你好" ); let hello = String ::from ("Olá" ); let hello = String ::from ("Здравствуйте" ); let hello = String ::from ("Hola" ); }
修改
push_str()
方法:把一个字符串切片附加到 String
1 2 3 4 5 fn main () { let mut s = String ::from ("foo" ); s.push_str ("bar" ); println! ("{}" , s); }
push()
方法:把单个字符附加到 String
1 2 3 4 5 fn main () { let mut s = String ::from ("foo" ); s.push ('b' ); println! ("{}" , s); }
1 2 3 4 5 6 fn main () { let s1 = String ::from ("I hate rust" ); let s2 = String ::from (", I love python" ); let s3 = s1 + &s2; println! ("{},{},{}" , s1, s2, s3); }
打印s1处会报错,说明s1的所有权被赋给了s3
1 2 3 4 5 6 7 fn main () { let s1 = String ::from ("tic" ); let s2 = String ::from ("tac" ); let s3 = String ::from ("toe" ); let s4 = format! ("{}-{}-{}" , s1, s2, s3); println! ("{}" , s4); }
长度
1 2 3 4 fn main () { let len = String ::from ("Hola" ).len (); println! ("{}" , len); }
1 2 3 4 fn main () { let len = String ::from ("你好" ).len (); println! ("{}" , len); }
索引
rust不允许对String进行索引
索引操作应消耗一个常量时间(O(1))
而String无法保证:需要遍历所有内容,来确定有多少个合法的字符。
切割字符串也要小心,可以使用 [ ]
和 一个范围 来创建字符串的切片
必须谨慎使用
如果切割时跨越了字符边界,程序就会panic。
遍历String
1 2 3 4 5 6 7 8 9 10 fn main () { let s = String ::from ("算法乐园" ); for i in s.chars () { println! ("{}" , i); } println! ("--------" ); for i in s.bytes () { println! ("{}" , i); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 算 法 乐园 -------- 231 174 151 230 179 149 228 185 144 229 155 173
HashMap
构造
1 2 3 4 5 6 use std::collections::HashMap;fn main () { let mut scores = HashMap::new (); scores.insert (String ::from ("CJL" ), 59 ); }
HashMap用的较少,不在Prelude中
标准库对其支持较少,没有内置的宏来创建 HashMap
数据存储在 heap 上
同构的。一个 HashMap 中:
所有的 K 必须是同一种类型
所有的 V 必须是同一种类型
使用collect方法构造
1 2 3 4 5 6 use std::collections::HashMap;fn main () { 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 (); }
所有权问题
对于实现了Copy trait的类型(例如i32) ,值会被复制到HashMap中
对于拥有所有权的值(例如String) ,值会被移动,所有权会转移给HashMap
如果将值的引用插入到 HashMap,值本身不会移动
在 HashMap 有效的期间,被引用的值必须保持有效
访问其中的值
1 2 3 4 5 6 7 8 9 10 11 12 use std::collections::HashMap;fn main () { 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); match score { Some (s) => println! ("{}" , s), None => println! ("team not exist" ), }; }
遍历
1 2 3 4 5 6 7 8 9 use std::collections::HashMap;fn main () { let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ), 10 ); scores.insert (String ::from ("Yellow" ), 50 ); for (k, v) in &scores { println! ("{}:{}" , k, v); } }
修改
HashMap 大小可变
每个K同时只能对应一个V
更新 HashMap 中的数据:
K 已经存在,对应一个 V
替换现有的 v
保留现有的 V,忽略新的 V
合并现有的 V 和新的 V
1 2 3 4 5 6 7 use std::collections::HashMap;fn main () { let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ), 10 ); scores.insert (String ::from ("Blue" ), 25 ); println! ("{:?}" , scores); }
K不存在
entry 方法:检查指定的K 是否对应一个V
参数为 K
返回 enum Entry:代表值是否存在
entry的 or_insert()方法:
返回:
如果K存在,返回到对应的V的一个可变引用
如果 K 不存在,将方法参数作为 K 的新值插进去,返回到这个值的可变引用
1 2 3 4 5 6 7 8 9 10 use std::collections::HashMap;fn main () { let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ), 10 ); let e = scores.entry (String ::from ("Yellow" )); println! ("{:?}" , e); e.or_insert (50 ); scores.entry (String ::from ("Blue" )).or_insert (50 ); println! ("{:?}" , scores); }
1 2 Entry (VacantEntry("Yellow" ) ) {"Blue" : 10 , "Yellow" : 50 }
1 2 3 4 5 6 7 8 9 10 use std::collections::HashMap;fn main () { let text = "What a beautiful girl she is!" ; let mut map = HashMap::new (); for word in text.split_whitespace () { let count = map.entry (word).or_insert (0 ); *count += 1 ; } println! ("{:#?}" , map); }
1 2 3 4 5 6 7 8 { "What" : 1 , "is!" : 1 , "beautiful" : 1 , "girl" : 1 , "she" : 1 , "a" : 1 , }
Hash函数
默认情况下, HashMap使用加密功能强大的Hash函数,可以抵抗拒绝服务(DoS)攻击。
可以指定不同的hasher来切换到另一个函数。
hasher 是实现 BuildHasher trait 的类型
第九章-错误处理
不可修复的错误与panic
1 2 3 fn main () { panic! ("crash and burn" ); }
1 2 3 4 thread 'main' panicked at src\main.rs:2:5: crash and burn note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceerror: process didn't exit successfully: `target\debug\variables.exe` (exit code: 101)
使用panic!产生的回溯信息
panic!可能出现在:
可通过调用 panic!的函数的回溯信息来定位引起问题的代码
通过设置环境变量set RUST_BACKTRACE=1
可得到回溯信息
如果你觉得回溯信息不够丰富,可以执行set RUST_BACKTRACE=full
为了获取带有调试信息的回溯,必须启用调试符号(不带–release)
Result枚举与可恢复的错误
1 2 3 4 enum Result <T,E>{ Ok (T), Err (E), }
打开文件hello.txt,从而产生错误
1 2 3 4 5 6 7 8 9 10 use std::fs::File;fn main () { let f = File::open ("hello.txt" ); let f = match f { Ok (file) => file, Err (error) => { panic! ("Error opening file {:?}" , error) } }; }
1 2 3 4 thread 'main' panicked at src\main.rs:7:13:Error opening file Os { code: 2, kind: NotFound, message: "系统找不到指 定的文件。" } note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceerror: process didn't exit successfully: `target\debug\variables.exe` (exit code: 101)
打开文件hello.txt, 如果hello.txt不存在,则新建文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use std::fs::File;use std::io::ErrorKind;fn main () { let f = File::open ("hello.txt" ); let f = match f { Ok (file) => file, Err (error) => match error.kind () { ErrorKind::NotFound => match File::create ("hello.txt" ) { Ok (fc) => fc, Err (e) => panic! ("Error creating file:{:?}" , e), }, oe => panic! ("Error opening the file:{:?}" , oe), }, }; }
expect自定义输出错误信息
1 2 3 4 use std::fs::File;fn main () { let f = File::open ("hello.txt" ).expect ("无法打开文件" ); }
返回错误给函数调用者处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use std::fs::File;use std::io;use std::io::Read;fn main () {}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), } }
传播错误的快捷方式
使用问号简化代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use std::fs::File;use std::io;use std::io::Read;fn main () {}fn read_username_from_file () -> Result <String , io::Error> { let mut f = File::open ("hello.txt" )?; let mut s = String ::new (); f.read_to_string (&mut s)?; Ok (s) }
?
与from函数
Trait std::convert::From上的from函数:
被?
所应用的错误,会隐式的被from函数处理
当?
调用 from 函数时:
它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型
用于: 针对不同错误原因,返回同一种错误类型
只要每个错误类型实现了转换为所返回的错误类型的from函数
?运算符只能用于返回Result的函数
上面的代码可以进一步简化
1 2 3 4 5 6 7 8 9 use std::fs::File;use std::io;use std::io::Read;fn main () {}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) }
在main函数中使用?运算符
main 函数返回类型是:()
main 函数的返回类型也可以是:Result<T, E>
Box<dyn Error>
是 trait 对象:
1 2 3 4 5 use std::{error::Error, fs::File};fn main () -> Result <(), Box <dyn Error>> { let f = File::open ("hello.txt" )?; Ok (()) }
何时使用panic!
在定义一个可能失败的函数时,优先考虑返回 Result
否则就 panic!
第十章-泛型-trait-生命周期
泛型
函数
1 2 3 4 5 6 7 8 fn lenth <T>(v: &[T]) -> i32 { v.len () as i32 }fn main () { let nums : Vec <i32 > = vec! [1 , 2 , 3 , 4 , 5 ]; let n = lenth (&nums); println! ("{}" , n); }
struct
1 2 3 4 5 6 7 struct Point <T>{ x:T, y:T, }fn main (){ let integer = Point{x:5 ,y:1 }; }
enum
1 2 3 4 5 6 7 enum Option <T> { Some (T), None , }fn main () { let op = Option ::Some (4 ); }
trait
Trait 告诉 Rust编译器:
Trait:抽象的定义共享行为
Trait bounds (约束) :泛型类型参数指定为实现了特定行为的类型
Trait与其它语言的接口(interface)类似,但有些区别。
Trait的定义:把方法签名放在一起,来定义实现某种目的所必需的一组行为。
关键字:trait
只有方法签名,没有具体实现
trait可以有多个方法:每个方法签名占一行,以;
结尾
实现该trait的类型必须提供具体的方法实现
1 2 3 4 5 pub trait Summary { fn summarize (&self ) -> String ; fn summarize1 (&self ) -> String ; }fn main () {}
示例:
lib.rs
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 pub trait Summary { fn summarize (&self )-> String ; }pub struct NewsArticle { pub headline:String , pub location:String , pub author:String , pub content:String , }impl Summary for NewsArticle { fn summarize (&self )-> String { format! ("{}, by {} ({})" ,self .headline,self .author,self .location) } }pub struct Tweet { pub username:String , pub content:String , pub reply:bool , pub retweet:bool , }impl Summary for Tweet { fn summarize (&self )-> String { format! ("{}:{}" ,self .username,self .content) } }
main.rs
1 2 3 4 5 6 7 8 9 10 11 12 use variables::Tweet;use variables::Summary;fn main () { let tweet = Tweet{ username:String ::from ("horse_ebooks" ), content:String ::from ("of course, as you probably already know, people" ), reply:false , retweet:false , }; println! ("1 new tweet:{}" ,tweet.summarize ()); }
注:这里的variables是本package的name, 在Cargo.toml
中定义
可以给trait Summary实现默认的函数summarize
默认实现的方法可以调用trait中其它的方法,即使这些方法没有默认实现。注意:无法从方法的重写实现里面调用默认的实现。
impl trait语法
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 27 28 29 pub trait Summary { fn summarize (&self )-> String ; }pub struct NewsArticle { pub headline:String , pub location:String , pub author:String , pub content:String , }impl Summary for NewsArticle { fn summarize (&self )-> String { format! ("{}, by {} ({})" ,self .headline,self .author,self .location) } }pub struct Tweet { pub username:String , pub content:String , pub reply:bool , pub retweet:bool , }impl Summary for Tweet { fn summarize (&self )-> String { format! ("{}:{}" ,self .username,self .content) } }pub fn notify (item:impl Summary ){ println! ("Breaking news!, {}" , item.summarize ()); }
trait bound语法
把上面的notify函数改成这样
1 2 3 pub fn notify <T: Summary>(item: T) { println! ("Breaking news!, {}" , item.summarize ()); }
指定多个trait
1 2 3 4 5 6 7 use std::fmt::Display;pub fn notify1 (item: impl Summary + Display) { println! ("Breaking news!, {}" , item.summarize ()); }pub fn notify2 <T: Summary + Display>(item: T) { println! ("Breaking news!, {}" , item.summarize ()); }
为了避免函数的签名过长,可以使用下面notify2
的写法
1 2 3 4 5 6 7 8 9 10 11 12 use std::fmt::Debug ;use std::fmt::Display;pub fn notify1 <T: Summary + Display, U: Clone + Debug >(item1: T, item2: U) -> String { format! ("Breaking news!, {}" , item1.summarize ()) }pub fn notify2 <T, U>(item1: T, item2: U) -> String where T: Summary + Display, U: Clone + Debug , { format! ("Breaking news!, {}" , item1.summarize ()) }
生命周期
这部分比较抽象,建议去看视频