modify some format errors
move something to another post
安装与调试 安装 Linux 情况下
1 2 3 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustc --version
调试 Linux 下使用
1 rust-gdb target/debug/your_program
Cargo 创建项目 cargo new 项目名称
帮助信息
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 Create a new cargo package at <path> Usage: cargo.exe new [OPTIONS] <path> Arguments: <path> Options: -q, --quiet Do not print cargo log messages --registry <REGISTRY> Registry to use --vcs <VCS> Initialize a new repository for the given version control system (git, hg, pijul, or fossil) or do not initialize any version control at all (none), overriding a global configuration. [possible values: git, hg, pijul, fossil, none] --bin Use a binary (application) template [default] -v, --verbose... Use verbose output (-vv very verbose/build.rs output) --lib Use a library template --color <WHEN> Coloring: auto, always, never --edition <YEAR> Edition to set for the crate generated [possible values: 2015, 2018, 2021] --frozen Require Cargo.lock and cache are up to date --name <NAME> Set the resulting package name, defaults to the directory name --locked Require Cargo.lock is up to date --offline Run without accessing the network --config <KEY=VALUE> Override a configuration value -Z <FLAG> Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details -h, --help Print help information Run `cargo help new` for more detailed information.
Cargo.toml
TOML(Tom’s Obvious,Minimal Language)格式,是 Cargo 的配置格式
1 2 3 4 5 6 7 8 9 [package] name = "hello" version = "0.1.0" authors = ["cauchy <731005515@qq.com>" ] edition = "2021" [dependencies]
在 Rust 中,代码的包叫做crate
构建 Cargo 项目
创建可执行文件 target/debug/hello_cargo 或 target\debug\hello_cargo.exe(Windows)
运行.\target\debug\hello_cargo.exe
第一次运行会生成 cargo.lock 文件
该文件负责追踪项目依赖的精确版本,不需要手动修改该文件
构建和运行 Cargo 项目
如果之前编译过且代码没有修改的话会直接执行
cargo check
检查代码,确保能通过编译,但是不产生任何可执行文件
cargo check 比 cargo build 快得多
发布构建
编译时会进行优化,代码运行的更快但是编译时间更长
会在 target/release 而不是 target/debug 生成可执行文件
变量与可变性 变量 声明使用let 关键字
默认情况下,变量是不可变的(immutable)
声明变量时,前面加上mut 关键字,就可以使变量可变
let mut x = 3;
常量 类似于不可变变量,常量(constants) 是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
1.不允许对常量使用 mut。常量不光默认不能变,它总是不能变。
2.声明常量使用 const 关键字而不是 let,并且 必须 注明值的类型 。
3.常量可以在任何作用域中声明 ,包括全局作用域,
4.最后一个区别是,常量只能被设置为常量表达式 ,而不可以是其他任何只能在运行时计算出的值 。
1 2 const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3 ;const MAX_POINTS:u32 = 100_000 ;
命名规范:全部大写,下划线间隔
隐藏(shadow) 1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let x = 5 ; let x = x + 1 ; { let x = x * 2 ; println! ("The value of x in the inner scope is: {x}" ); } println! ("The value of x is: {x}" ); }
我们可以定义一个与之前变量同名的新变量,新的变量会 shadow 之前声明的同名变量
shadow 和把变量标记为 mut是不一样的
如果不适用 let 关键字,那么给非 mut 的变量赋值会导致编译时错误
使用 let 声明的同名新变量,也是不可变的
使用 let 声明的同名新变量,他的类型可以与之前不同
允许未使用的变量 两种方式
1 2 3 4 5 6 7 fn main () { let _x = 1 ; } #[allow(unused_variables)] fn main () { let x = 1 ; }
数据类型 Rust 是静态编译语言 ,编译时必须知道所有变量的类型
标量类型 Rust 有四种基本的标量类型:整型 、浮点型 、布尔类型 和字符类型
整数类型 如果我们没有显式的给予变量一个类型,那编译器会自动帮我们推导一个类型
1 2 3 4 5 6 7 8 fn main (){ let x = 5 ; assert_eq! ("i32" .to_string (),type_of (&x)); } fn type_of <T>(_: &T) -> String { format! ("{}" ,std::any::type_name::<T>()) }
整数如果不赋予类型默认为 i32 类型
1 2 3 fn main () { let v : u16 = 38_u8 as u16 ; }
长度
有符号
无符号
8-bit
i8
u8
16-bit
i16
u16
32-bit
i32
u32
64-bit
i64
u64
128-bit
i128
u128
arch
isize
usize
isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。
1 2 3 4 fn main () { assert_eq! (i8 ::MAX, 127 ); assert_eq! (u8 ::MAX, 255 ); }
整型字面值
数字字面值
例子
Decimal (十进制)
98_222
Hex (十六进制)
0xff
Octal (八进制)
0o77
Binary (二进制)
0b1111_0000
Byte (单字节字符)(仅限于 u8)
b’A’
Rust 的数字类型默认是 i32。isize 或 usize 主要作为某些集合的索引。
整形溢出
比方说有一个 u8 ,它可以存放从零到 255 的值。那么当你将其修改为 256 时会发生什么呢?这被称为 “整型溢出”(integer overflow ),这会导致以下两种行为之一的发生。当在 debug 模式编译时,Rust 检查这类问题并使程序 panic ,这个术语被 Rust 用来表明程序因错误而退出。
在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码回绕(two’s complement wrapping )的操作 。简而言之,比此类型能容纳最大值还大的值会回绕到最小值,值 256 变成 0,值 257 变成 1,依此类推。依赖整型回绕被认为是一种错误,即便可能出现这种行为。如果你确实需要这种行为,标准库中有一个类型显式提供此功能,Wrapping。 为了显式地处理溢出的可能性,你可以使用标准库在原生数值类型上提供的以下方法:
所有模式下都可以使用 wrapping_* 方法进行回绕 ,如 wrapping_add
如果 checked_* 方法 出现溢出,则返回 None 值
用 overflowing_* 方法 返回值和一个布尔值,表示是否出现溢出
用 saturating_* 方法 在值的最小值或最大值处进行饱和处理
1 2 3 4 5 6 fn main () { let v1 = 251_u8 + 8 ; let v2 = i8 ::checked_add (251 , 8 ).unwrap (); println! ("{},{}" ,v1,v2); }
修改
1 2 3 4 5 6 7 8 9 10 11 fn main () { let v1 = 247_u8 + 8 ; let v2 = i8 ::checked_add (119 , 8 ).unwrap (); println! ("{},{}" ,v1,v2); } #[allow(unused_variables)] fn main () { let v1 = 251_u16 + 8 ; let v2 = u16 ::checked_add (251 , 8 ).unwrap (); println! ("{},{}" ,v1,v2); }
浮点类型 Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。所有的浮点型都是有符号的。
1 2 3 4 5 fn main () { let x = 2.0 ; let y : f32 = 3.0 ; }
浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。
1 2 3 4 5 fn main () { let x = 1_000.000_1 ; let y : f32 = 0.12 ; let z = 0.01_f64 ; }
数值运算
Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余。整数除法会向下舍入 到最接近的整数
1 2 3 4 5 6 fn main () { assert_eq! (0.1 +0.2 ,0.3 ); } thread 'mai n' panicked at ' assertion failed: `(left == right)` left: `0.30000000000000004 `, right: `0.3 `', src\m ain.rs:5:5
两种修改方法
1 2 3 4 5 6 fn main () { assert! (0.1 +0.2 >=0.3 ); } fn main () { assert! (0.1_f32 +0.2_f32 ==0.3_f32 ); }
计算
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 32 33 34 use std::fmt::Display;#[allow(unused_variables)] fn print_something <T >(something:T)where T : Display{ println! ("{}" ,something); } fn main () { print_something (1u32 + 2 ); print_something (1i32 - 2 ); print_something (1i8 - 2 ); print_something (3 * 50 ); print_something (9 / 3 == 3 ); print_something (24 % 5 ); print_something (true && false ); print_something (true || false ); print_something (!true ); println! ("0011 AND 0101 is {:04b}" , 0b0011u32 & 0b0101 ); println! ("0011 OR 0101 is {:04b}" , 0b0011u32 | 0b0101 ); println! ("0011 XOR 0101 is {:04b}" , 0b0011u32 ^ 0b0101 ); println! ("1 << 5 is {}" , 1u32 << 5 ); println! ("0x80 >> 2 is 0x{:x}" , 0x80u32 >> 2 ); }
序列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn main () { let mut sum = 0 ; for i in -3 ..2 { println! ("i is {}" ,i); } for c in 'a' ..='z' { println! ("{}" ,c); } } #[allow(unused_variables)] use std::ops::{Range, RangeInclusive};fn main () { assert_eq! ((1 ..5 ), Range{ start: 1 , end: 5 }); assert_eq! ((1 ..=5 ), RangeInclusive::new (1 , 5 )); }
布尔类型 Rust 中的布尔类型使用 bool 表示
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let t = true ; let f : bool = false ; } fn main () { let f = true ; let t = true && false || true ; assert_eq! (t, f); println! ("Success!" ) }
字符类型 Rust 的 char 类型是语言中最原生的字母类型
1 2 3 4 5 6 7 8 9 10 11 12 13 fn main () { let c = 'z' ; let z : char = 'ℤ' ; let heart_eyed_cat = '😻' ; } fn main () { let c1 = '中' ; print_char (c1); } fn print_char (c : char ) { println! ("{}" , c); }
用单引号声明 char 字面量,而与之相反的是,使用双引号声明字符串字面量。Rust 的 char 类型的大小为四个字节 (four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。在 Rust 中,带变音符号的字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。Unicode 标量值包含从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char 并不符合。
大小
1 2 3 4 5 6 7 8 9 10 11 12 #[allow(unused_variables)] use std::mem::size_of_val;fn main () { let c1 = 'a' ; assert_eq! (size_of_val (&c1),4 ); let c2 = '中' ; assert_eq! (size_of_val (&c2),4 ); println! ("Success!" ) }
单元类型 1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let _v : () = (); let v = (2 , 3 ); assert_eq! (_v, implicitly_ret_unit ()); println! ("Success!" ) } fn implicitly_ret_unit () { println! ("I will return a ()" ) }
单元类型所占的内存为 0!!!
1 2 3 4 5 6 7 use std::mem::size_of_val;fn main () { let unit : () = (); assert! (size_of_val (&unit) == 0 ); println! ("Success!" ) }
复合类型 复合类型 (Compound types )可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
元组类型 元组长度固定 :一旦声明,其长度不会增大或缩小
使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的
1 2 3 fn main () { let tup : (i32 , f64 , u8 ) = (500 , 6.4 , 1 ); }
tup 变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配 (pattern matching)来解构 (destructure)元组值,像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn main () { let tup = (500 , 6.4 , 1 ); let (x, y, z) = tup; println! ("The value of y is: {y}" ); } fn main () { let (x, y, z); (y,z,x) = (1 , 2 , 3 ); assert_eq! (x, 3 ); assert_eq! (y, 1 ); assert_eq! (z, 2 ); }
程序首先创建了一个元组并绑定到 tup 变量上。接着使用了 let 和一个模式将 tup 分成了三个不同的变量,x、y 和 z。这叫做 解构 (destructuring ),因为它将一个元组拆成了三个部分。
也可以使用点号(.)后跟值的索引来直接访问它们 。元组的第一个索引值是 0。例如:
1 2 3 4 5 6 7 8 9 fn main () { let x : (i32 , f64 , u8 ) = (500 , 6.4 , 1 ); let five_hundred = x.0 ; let six_point_four = x.1 ; let one = x.2 ; }
不带任何值的元组有个特殊的名称,叫做 单元(unit) 元组。这种值以及对应的类型都写作 (),表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。
过长的元组无法打印
1 2 3 4 5 6 7 8 9 10 fn main () { let too_long_tuple = (1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 ); println! ("too long tuple: {:?}" , too_long_tuple); } fn main () { let too_long_tuple = (1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ); println! ("too long tuple: {:?}" , too_long_tuple); }
数组类型 与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,Rust 中的数组长度是固定的。
数组的类型是[T; Length] ,数组的长度是类型签名的一部分,因此数组的长度必须在编译期就已知,
vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,那么很可能应该使用 vector。
可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。
let a: [i32; 5] = [1, 2, 3, 4, 5];
这里,i32 是每个元素的类型。分号之后,数字 5 表明该数组包含五个元素。
1 2 3 4 5 6 7 8 9 fn main () { let arr0 = [1 , 2 , 3 ]; let arr : [char ; 3 ] = ['a' , 'b' , 'c' ]; assert! (std::mem::size_of_val (&arr) == 12 ); }
还可以通过在方括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组 :
变量名为 a 的数组将包含 5 个元素,这些元素的值最初都将被设置为 3。这种写法与 let a = [3, 3, 3, 3, 3]; 效果相同,但更简洁。
访问数组元素
数组是可以在栈(stack)上分配的已知固定大小的单个内存块。可以使用索引来访问数组的元素,像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fn main () { let a = [1 , 2 , 3 , 4 , 5 ]; let first = a[0 ]; let second = a[1 ]; } fn main () { let names = [String ::from ("Sunfei" ), "Sunface" .to_string ()]; let name0 = names.get (0 ).unwrap (); let _name1 = &names[1 ]; }
无效的数组访问
如果我们访问数组结尾之后的元素,程序在索引操作中使用一个无效的值时导致 运行时 错误。程序带着错误信息退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 panic ,这是 Rust 术语,它用于程序因为错误而退出的情况。
这种检查必须在运行时进行 ,特别是在某些情况下,因为编译器不可能知道用户在以后运行代码时将输入什么值。
类型转换 使用 as 进行基本类型转换 1.Rust 并没有为基本类型提供隐式的类型转换( coercion ) ,但是我们可以通过 as 来进行显式地转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #[allow(unused_variables)] fn main () { let decimal = 97.123_f32 ; let integer : u8 = decimal as u8 ; let c1 : char = decimal as u8 as char ; let c2 = integer as char ; println! ("c1 is {}" ,c1); assert_eq! (integer, 'b' as u8 - 1 ); println! ("Success!" ) }
2.默认情况下, 数值溢出会导致编译错误 ,但是我们可以通过添加一行全局注解 #![allow(overflowing_literals)] 的方式来避免编译错误(溢出还是会发生)
1 2 3 4 5 #![allow(overflowing_literals)] fn main () { assert_eq! (u8 ::MAX, 255 ); let v = 1000 as u8 ; }
3.当将任何数值转换成无符号整型 T 时,如果当前的数值不在新类型的范围内,我们可以对当前数值进行加值或减值操作( 增加或减少 T::MAX + 1 ) ,直到最新的值在新类型的范围内,假设我们要将 300 转成 u8 类型,由于 u8 最大值是 255,因此 300 不在新类型的范围内并且大于新类型的最大值,因此我们需要减去 T::MAX + 1,也就是 300 - 256 = 44。
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 #![allow(overflowing_literals)] fn main () { assert_eq! (1000 as u16 , 1000 ); assert_eq! (1000 as u8 , 232 ); println! ("1000 mod 256 is : {}" , 1000 % 256 ); assert_eq! (-1_i8 as u8 , 255 ); assert_eq! (300.1_f32 as u8 , 255 ); assert_eq! (-100.1_f32 as u8 , 0 ); unsafe { println! ("300.0 is {}" , 300.0_f32 .to_int_unchecked::<u8 >()); println! ("-100.0 as u8 is {}" , (-100.0_f32 ).to_int_unchecked::<u8 >()); println! ("nan as u8 is {}" , f32 ::NAN.to_int_unchecked::<u8 >()); } }
4.裸指针可以和代表内存地址的整数互相转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 fn main () { let mut values : [i32 ; 2 ] = [1 , 2 ]; let p1 : *mut i32 = values.as_mut_ptr (); let first_address = p1 as usize ; let second_address = first_address + 4 ; let p2 = second_address as *mut i32 ; unsafe { *p2 += 1 ; } assert_eq! (values[1 ], 3 ); println! ("Success!" ) } fn main () { let arr :[u64 ; 13 ] = [0 ; 13 ]; assert_eq! (std::mem::size_of_val (&arr), 8 * 13 ); let a : *const [u64 ] = &arr; let b = a as *const [u8 ]; unsafe { assert_eq! (std::mem::size_of_val (&*b), 13 ) } }
From/Into
From 特征允许让一个类型定义如何基于另一个类型来创建自己 ,因此它提供了一个很方便的类型转换的方式。
From 和 Into 是配对的,我们只要实现了前者,那后者就会自动被实现 :只要实现了 impl From for U, 就可以使用以下两个方法: let u: U = U::from(T) 和 let u:U = T.into(),前者由 From 特征提供,而后者由自动实现的 Into 特征提供。
需要注意的是,当使用 into 方法时,需要进行显式地类型标注,因为编译器很可能无法帮我们推导出所需的类型。
例子
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 fn main () { let my_str = "hello" ; let string1 = String ::from (my_str); let string2 = my_str.to_string (); let string3 : String = my_str.into (); } fn main () { let i1 :i32 = false .into (); let i2 :i32 = i32 ::from (false ); assert_eq! (i1, i2); assert_eq! (i1, 0 ); let i3 : i32 = 'a' .into (); let s : String = 'a' as String ; println! ("Success!" ) } fn main () { let i1 :i32 = false .into (); let i2 :i32 = i32 ::from (false ); assert_eq! (i1, i2); assert_eq! (i1, 0 ); let i3 :u32 = 'a' .into (); let s : String = 'a' .into (); println! ("Success!" ) } fn main () { let i1 :i32 = false .into (); let i2 :i32 = i32 ::from (false ); assert_eq! (i1, i2); assert_eq! (i1, 0 ); let i3 : u32 = 'a' as u32 ; let s : String = String ::from ('a' ); }
为自定义类型实现 From 特征 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 #[derive(Debug)] struct Number { value: i32 , } impl From <i32 > for Number { fn from (item: i32 ) -> Self { Number { value: item } } } fn main () { let num = Number::from (30 ); assert_eq! (num.value, 30 ); let num : Number = 30 .into (); assert_eq! (num.value, 30 ); println! ("Success!" ) }
当执行错误处理时,为我们自定义的错误类型实现 From 特征是非常有用。这样就可以通过 ? 自动将某个错误类型转换成我们自定义的错误类型
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 32 use std::fs;use std::io;use std::num;enum CliError { IoError (io::Error), ParseError (num::ParseIntError), } impl From <io::Error> for CliError { fn from (error: io::Error) -> Self { CliError::IoError (error) } } impl From <num::ParseIntError> for CliError { fn from (error: num::ParseIntError) -> Self { CliError::ParseError (error) } } fn open_and_parse_file (file_name: &str ) -> Result <i32 , CliError> { let contents = fs::read_to_string (&file_name)?; let num : i32 = contents.trim ().parse ()?; Ok (num) } fn main () { println! ("Success!" ) }
TryFrom / TryInto 类似于 From 和 Into, TryFrom 和 TryInto 也是用于类型转换的泛型特征。
但是又与 From/Into 不同, TryFrom 和 TryInto 可以对转换后的失败进行处理,然后返回一个 Result 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn main () { let n : i16 = 256 ; let n : u8 = match n.try_into () { Ok (n) => n, Err (e) => { println! ("there is an error when converting: {:?}, but we catch it" , e.to_string ()); 0 } }; assert_eq! (n, 0 ); println! ("Success!" ) }
自定义实现
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 #[derive(Debug, PartialEq)] struct EvenNum (i32 );impl TryFrom <i32 > for EvenNum { type Error = (); fn try_from (value: i32 ) -> Result <Self , Self ::Error> { if value % 2 == 0 { Ok (EvenNum (value)) } else { Err (()) } } } fn main () { assert_eq! (EvenNum::try_from (8 ), Ok (EvenNum (8 ))); assert_eq! (EvenNum::try_from (5 ), Err (())); let result : Result <EvenNum, ()> = 8i32 .try_into (); assert_eq! (result, Ok (EvenNum (8 ))); let result : Result <EvenNum, ()> = 5i32 .try_into (); assert_eq! (result,Err (())); println! ("Success!" ) }
其它转换 将任何类型转换成 String 只要为一个类型实现了 ToString,就可以将任何类型转换成 String。事实上,这种方式并不是最好的,可以利用 fmt::Display trait?它可以控制一个类型如何打印,在实现它的时候还会自动实现 ToString。因为 to_string 是基于 fmt::Display 实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 use std::fmt;struct Point { x: i32 , y: i32 , } impl fmt ::Display for Point { fn fmt (&self , f: &mut fmt::Formatter<'_ >) -> fmt::Result { write! (f, "The point is ({}, {})" , self .x, self .y) } } fn main () { let origin = Point { x: 0 , y: 0 }; assert_eq! (origin.to_string (), "The point is (0, 0)" ); assert_eq! (format! ("{}" , origin), "The point is (0, 0)" ); println! ("Success!" ) }
解析 String 使用 parse 方法可以将一个 String 转换成 i32 数字,这是因为在标准库中为 i32 类型实现了 FromStr: : impl FromStr for i32
1 2 3 4 5 6 7 8 9 10 11 use std::str ::FromStr;fn main () { let parsed : i32 = "5" .parse ().unwrap (); let turbo_parsed = "10" .parse::<i32 >().unwrap (); let from_str = i32 ::from_str ("20" ).unwrap (); let sum = parsed + turbo_parsed + from_str; assert_eq! (sum, 35 ); println! ("Success!" ) }
自定义实现 FromStr 特征 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 use std::str ::FromStr;use std::num::ParseIntError;#[derive(Debug, PartialEq)] struct Point { x: i32 , y: i32 } impl FromStr for Point { type Err = ParseIntError; fn from_str (s: &str ) -> Result <Self , Self ::Err > { let coords : Vec <&str > = s.trim_matches (|p| p == '(' || p == ')' ) .split (',' ) .collect (); let x_fromstr = coords[0 ].parse::<i32 >()?; let y_fromstr = coords[1 ].parse::<i32 >()?; Ok (Point { x: x_fromstr, y: y_fromstr }) } } fn main () { let p = "(3,4)" .parse::<Point>(); assert_eq! (p.unwrap (), Point{ x: 3 , y: 4 } ) }
transmute std::mem::transmute 是一个 unsafe 函数,可以把一个类型按位解释为另一个类型 ,其中这两个类型必须有同样的位数( bits ) 。
transmute 相当于将一个类型按位移动到另一个类型,它会将源值的所有位拷贝到目标值中,然后遗忘源值。该函数跟 C 语言中的 memcpy 函数类似。
正因为此,transmute 非常非常不安全! 调用者必须要自己保证代码的安全性,当然这也是 unsafe 的目的。
示例
1.transmute 可以将一个指针转换成一个函数指针,该转换并不具备可移植性,原因是在不同机器上,函数指针和数据指针可能有不同的位数( size )。
1 2 3 4 5 6 7 8 9 10 11 fn foo () -> i32 { 0 } fn main () { let pointer = foo as *const (); let function = unsafe { std::mem::transmute::<*const (), fn () -> i32 >(pointer) }; assert_eq! (function (), 0 ); }
2.transmute 还可以扩展或缩短一个不变量的生命周期,即生命周期的“非法转换 !
1 2 3 4 5 6 7 8 9 10 11 struct R <'a >(&'a i32 );unsafe fn extend_lifetime <'b >(r: R<'b >) -> R<'static > { std::mem::transmute::<R<'b >, R<'static >>(r) } unsafe fn shorten_invariant_lifetime <'b , 'c >(r: &'b mut R<'static >) -> &'b mut R<'c > { std::mem::transmute::<&'b mut R<'static >, &'b mut R<'c >>(r) }
3.事实上我们还可以使用一些安全的方法来替代 transmute.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 fn main () { let raw_bytes = [0x78 , 0x56 , 0x34 , 0x12 ]; let num = unsafe { std::mem::transmute::<[u8 ; 4 ], u32 >(raw_bytes) }; let num = u32 ::from_ne_bytes (raw_bytes); let num = u32 ::from_le_bytes (raw_bytes); assert_eq! (num, 0x12345678 ); let num = u32 ::from_be_bytes (raw_bytes); assert_eq! (num, 0x78563412 ); let ptr = &0 ; let ptr_num_transmute = unsafe { std::mem::transmute::<&i32 , usize >(ptr) }; let ptr_num_cast = ptr as *const i32 as usize ; let ptr = &mut 0 ; let val_transmuted = unsafe { std::mem::transmute::<&mut i32 , &mut u32 >(ptr) }; let val_casts = unsafe { &mut *(ptr as *mut i32 as *mut u32 ) }; let slice = unsafe { std::mem::transmute::<&str , &[u8 ]>("Rust" ) }; assert_eq! (slice, &[82 , 117 , 115 , 116 ]); let slice = "Rust" .as_bytes (); assert_eq! (slice, &[82 , 117 , 115 , 116 ]); assert_eq! (b"Rust" , &[82 , 117 , 115 , 116 ]); }
函数 Rust 代码中的函数和变量名 使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词
我们在 Rust 中通过输入 fn 后面跟着函数名和一对圆括号来定义函数。大括号告诉编译器哪里是函数体的开始和结尾。
参数 我们可以定义为拥有 参数 (parameters )的函数,参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为参数(arguments )
1 2 3 4 5 6 7 fn main () { another_function (5 ); } fn another_function (x: i32 ) { println! ("The value of x is: {x}" ); }
当定义多个参数时,使用逗号分隔
语句与表达式 函数体由一系列的语句和一个可选的结尾表达式构成。
语句 (Statements )是执行一些操作但不返回值的指令 。
表达式 (Expressions )计算并产生一个值 。
语句不返回值,表达式会计算出一个值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn main () { let x = 5u32 ; let y = { let x_squared = x * x; let x_cube = x_squared * x; x_cube + x_squared + x }; let z = { 2 * x; }; println! ("x is {:?}" , x); println! ("y is {:?}" , y); println! ("z is {:?}" , z); }
语句 let y = 6; 中的 6 是一个表达式,它计算出的值是 6。函数调用是一个表达式 。宏调用是一个表达式 。用大括号创建的一个新的块作用域也是一个表达式 ,例如:
1 2 3 4 5 6 7 8 fn main () { let y = { let x = 3 ; x + 1 }; println! ("The value of y is: {y}" ); }
这个表达式:
是一个代码块,它的值是 4。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。
返回值 不对返回值命名,但要在箭头(->)后声明它的类型
在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用 return 关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式
1 2 3 4 5 6 7 8 9 fn main () { let x = plus_one (5 ); println! ("The value of x is: {x}" ); } fn plus_one (x: i32 ) -> i32 { x + 1 }
返回类型为() 1 2 3 4 5 6 7 8 9 fn main (){ println! ("{}" ,type_of (&println! ("helloworld" ))) } fn type_of <T>(_: &T) -> String { format! ("{}" ,std::any::type_name::<T>()) }
返回类型为 never 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use std::thread;use std::time;fn never_return () -> ! { loop { println! ("I return nothing" ); thread::sleep (time::Duration::from_secs (1 )) } } fn main () { never_return (); }
发散函数(Diverging function) 发散函数( Diverging function )不会返回任何值,因此它们可以用于替代需要返回任何值的地方
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 fn main () { println! ("Success!" ); } fn get_option (tp: u8 ) -> Option <i32 > { match tp { 1 => { } _ => { } }; never_return_fn () } fn never_return_fn () -> ! {} fn main () { println! ("Success!" ); } fn get_option (tp: u8 ) -> Option <i32 > { match tp { 1 => { } _ => { } }; never_return_fn () } fn never_return_fn () -> ! { unimplemented! () } fn never_return_fn () -> ! { panic! () } fn never_return_fn () -> ! { todo!(); } fn never_return_fn () -> ! { loop { std::thread::sleep (std::time::Duration::from_secs (1 )) } }
The difference between unimplemented! and [todo] is that while todo! conveys an intent of implementing the functionality later and the message is “not yet implemented”, unimplemented! makes no such claims. Its message is “not implemented” . Also some IDEs will mark todo!s.
调用
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 #[allow(unused)] fn main () { get_option (3 ); println! ("Success!" ); } fn get_option (tp: u8 ) -> Option <i32 > { match tp { 1 => { } _ => { } }; never_return_fn () } fn never_return_fn () -> ! { loop { std::thread::sleep (std::time::Duration::from_secs (1 )) } }
使用 unimplemented!()和 todo!();会报以下错误
thread ‘main’ panicked at ‘not implemented’, src\main.rs:24:5
1 2 3 4 5 6 7 8 let _v = match b { true => 1 , false => { println! ("Success!" ); panic! ("we have no value for `false`, but we can panic" ) } };
控制流 if 表达式 1 2 3 4 5 6 7 8 9 fn main () { let number = 3 ; if number < 5 { println! ("condition was true" ); } else { println! ("condition was false" ); } }
if 表达式中与条件关联的代码块有时被叫做 *arms
if/else 可以用作表达式来进行赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fn main () { let n = 5 ; let big_n = if n < 10 && n > -10 { println! (" 数字太小,先增加 10 倍再说" ); 10 * n } else { println! ("数字太大,我们得让它减半" ); n / 2 }; println! ("{} -> {}" , n, big_n); }
注意
1.代码中的条件必须是 bool 值。如果条件不是 bool 值,我们将得到一个错误。Rust 并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为 if 的条件。
2.如果使用了多于 1 个 else if 最好使用 match 对代码进行重构
在 let 语句中使用 if 因为 if 是一个表达式,我们可以在 let 语句的右侧使用它
1 2 3 4 5 6 fn main () { let condition = true ; let number = if condition { 5 } else { 6 }; println! ("The value of number is: {number}" ); }
if 的每个分支的可能的返回值都必须是相同类型
注意
if 代码块中的表达式返回一个整数,而 else 代码块中的表达式返回一个字符串。这不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道变量的类型
循环 loop 1 2 3 4 5 fn main () { loop { println! ("again!" ); } }
从循环中返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 fn main () { let mut counter = 0 ; let result = loop { counter += 1 ; if counter == 10 { break counter * 2 ; } }; println! ("The result is {result}" ); }
循环标签 如果存在嵌套循环,break 和 continue 应用于此时最内层的循环。你可以选择在一个循环上指定一个 循环标签 (loop label ),然后将标签与 break 或 continue 一起使用,使这些关键字应用于已标记的循环而不是最内层的循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 fn main () { let mut count = 0 ; 'counting_up : loop { println! ("count = {count}" ); let mut remaining = 10 ; loop { println! ("remaining = {remaining}" ); if remaining == 9 { break ; } if count == 2 { break 'counting_up ; } remaining -= 1 ; } count += 1 ; } println! ("End count = {count}" ); }
while 1 2 3 4 5 6 7 8 9 10 11 fn main () { let mut number = 3 ; while number != 0 { println! ("{number}!" ); number -= 1 ; } println! ("LIFTOFF!!!" ); }
for 1 2 3 4 5 6 7 fn main () { let a = [10 , 20 , 30 , 40 , 50 ]; for element in a { println! ("the value is: {element}" ); } }
for 循环遍历集合元素相较于 while 循环,增强了代码安全性,并消除了可能由于超出数组的结尾或遍历长度不够而缺少一些元素而导致的 bug
对于没有实现 copy 的可迭代对象 for in 会取得所有权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fn main () { let names = [String ::from ("liming" ),String ::from ("hanmeimei" )]; for name in &names { } println! ("{:?}" , names); let numbers = [1 , 2 , 3 ]; for n in numbers { } println! ("{:?}" , numbers); }
通过索引和值的方式迭代数组 1 2 3 4 5 6 7 8 fn main () { let a = [4 ,3 ,2 ,1 ]; for (i,v) in a.iter ().enumerate () { println! ("第{}个元素是{}" ,i+1 ,v); } }
Range
它是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列。(不包括结束的数字)
rev 方法可以反转 range
1 2 3 4 5 6 fn main () { for number in (1 ..4 ).rev () { println! ("{number}!" ); } println! ("LIFTOFF!!!" ); }
所有权,引用与借用 栈(Stack)与堆(Heap) 栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作 后进先出 (last in, first out )。
增加数据叫做 进栈 (pushing onto the stack ),而移出数据叫做 出栈 (popping off the stack )。栈中的所有数据都必须占用已知且固定的大小。
在编译时大小未知或大小可能变化的数据,要存储在堆上 。 堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针 (pointer )。这个过程称作 在堆上分配内存 (allocating on the heap ),有时简称为 “分配”(allocating)。(将数据推入栈中并不被认为是分配)。因为指向放入堆中数据的指针是已知的并且大小是固定的,你可以将该指针存储在栈上 ,不过当需要实际数据时,必须访问指针。
入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。 相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存),出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。
当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的主要目的就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。
所有权规则
Rust 中的每一个值都有一个 所有者(owner)
值在任一时刻有且只有一个所有者。
当所有者(变量)离开作用域,这个值将被丢弃。
所有权的例子:
修改下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 fn main () { let s = give_ownership (); println! ("{}" , s); } fn give_ownership () -> String { let s = String ::from ("hello, world" ); let _s = s.into_bytes (); s }
方法
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let s = give_ownership (); println! ("{}" , s); } fn give_ownership () -> String { let s = String ::from ("hello, world" ); let _s = s.as_bytes (); s }
或
1 2 3 4 5 6 7 8 9 10 fn main () { let s = give_ownership (); println! ("{}" , s); } fn give_ownership () -> String { let s = String ::from ("hello, world" ); s }
当所有权转移时,可变性也可以随之改变。 1 2 3 4 5 6 7 fn main () { let s = String ::from ("hello, " ); let mut s1 = s; s1.push_str ("world" ) }
变量与作用域 作用域是一个项(item)在程序中有效的范围。假设有这样一个变量:
let s = “hello”;
变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码 进程序代码中的。这个变量从声明的点开始直到当前 作用域 结束时都是有效的。示例 4-1 中的注释标明了变量 s 在何处是有效的。
1 2 3 4 5 { let s = "hello" ; }
当 s进入作用域 时,它就是有效的。
这一直持续到它 离开作用域 为止。
str 和&str 正常情况下我们无法使用 str 类型,但是可以使用 &str 来替代
1 2 3 fn main () { let s : &str = "hello, world" ; }
如果要使用 str 类型,只能配合 Box。
1 2 3 4 5 6 7 8 fn main () { let s : Box <str > = "hello, world" .into (); greetings (s) } fn greetings (s: Box <str >) { println! ("{}" ,s) }
& 可以用来将 Box 转换为 &str 类型 , Rust 的 Deref coercion 会把 &Box<str> 自动转换为 &str
1 2 3 4 5 6 7 8 fn main () { let s : Box <str > = "hello, world" .into (); greetings (&s) } fn greetings (s: &str ) { println! ("{}" ,s) }
String 类型 String 是定义在标准库中的类型,分配在堆上,可以动态的增长。它的底层存储是动态字节数组的方式( Vec ),但是与字节数组不同,String 是 UTF-8 编码 。
Unicode 和编码方式的区别
Unicode
是一个字符集(character set),规定了每个字符对应一个唯一的 码点(code point)
码点形式:U+0000 ~ U+10FFFF
例如:
UTF-8 / UTF-16 / UTF-32
是把 Unicode 码点转换成 字节序列 的具体方法
也就是说,Unicode 是“字符表”,UTF-8 是“如何存储或传输这些字符的编码规则”
String.chars()和 String.bytes()分别以 Unicode 字符和字节遍历。
一个 Unicode 字符的长度不是固定的
一个 Unicode 字符并不一定是一个完整显示的字符
在 Unicode 和文本处理里,字符簇(grapheme cluster) 是一个用户感知的“字符单位”,也就是说,它是用户看到的一个完整字符,但它可能由 多个 Unicode 标量值(char)组成 。
简单来说:
一个字符簇 ≈ “一个完整显示字符”
不同于 Rust 的 char,char 是单个 Unicode 标量值(可能是一个字母、一个汉字、或一个 emoji 的组成部分)
一个字符簇可能包含:
基础字符 + 组合符号(比如重音符)
emoji 组合(如 👨👩👧👦 家庭表情,由多个 emoji 和零宽连接符组成)
要遍历字符簇,需要第三方包,比如:
String 管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本。可以使用 from 函数基于字符串字面值来创建 String
1 let s = String ::from ("hello" );
可以 修改此类字符串 :
1 2 3 4 5 let mut s = String ::from ("hello" ); s.push_str (", world!" ); println! ("{}" , s);
内存与分配 就字符串字面值 来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件 中。这使得字符串字面值快速且高效。不过这些特性都只得益于字符串字面值的不可变性。不幸的是,我们不能为了每一个在编译时大小未知的文本而将一块内存放入二进制文件中,并且它的大小还可能随着程序运行而改变。
对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:
必须在运行时向内存分配器(memory allocator)请求内存。
需要一个当我们处理完 String 时将内存返回给分配器的方法。(某些语言的垃圾回收 GC)
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放 。下面是示例 4-1 中作用域例子的一个使用 String 而不是字符串字面值的版本:
1 2 3 4 5 6 { let s = String::from("hello"); // 从此处起,s 是有效的 // 使用 s } // 此作用域已结束, // s 不再有效
这是一个将 String 需要的内存返回给分配器的很自然的位置:当 s 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop ,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop。
变量与数据交互的方式 move 栈数据
将 5 绑定到 x;接着生成一个值 x 的拷贝并绑定到 y”。现在有了两个变量,x 和 y,都等于 5
因为整数是有已知固定大小的简单值,所以这两个 5 被放入了栈中。
对于此类数据,移动和克隆没有区别
1 2 let s1 = String ::from ("hello" );let s2 = s1;
一个 String 由 3 部分组成:
一个指向存放字符串内容的内存的指针
一个长度 len,指存放字符串内容所需的字节数
一个容量 capacity,指 String 从操作系统中总共获得内存的总字节数
上面这些存放在栈上 ,存放字符串内容的部分存放在堆上
当我们将 s1 赋值给 s2,如果 String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。
当变量离开时,会调用 drop,导致 double free
为了保证内存安全
Rust 没有尝试复制被分配的内存
Rust 让 s1 失效,即变量 s1 离开作用域时不需要释放任何东西(对应所有权规则 2:值在任一时刻有且只有一个所有者)
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 let s1 = String ::from ("hello" ); let s2 = s1; println! ("{}, world!" , s1); warning: unused variable: `s2` --> src\main.rs:3 :9 | 3 | let s2 = s1; | ^^ help: if this is intentional, prefix it with an underscore: `_s2` | = note: `#[warn(unused_variables)] ` on by default error[E0382]: borrow of moved value: `s1` --> src\main.rs:5 :28 | 2 | let s1 = String ::from ("hello" ); | -- move occurs because `s1` has type `String `, which does not implement the `Copy ` trait 3 | let s2 = s1; | -- value moved here 4 |5 | println! ("{}, world!" , s1); | ^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro -backtrace for more in fo) For more information about this error, try `rustc --explain E0382`. warning: `loop_test` (bin "loop_test" ) generated 1 warning error: could not compile `loop_test` due to previous error; 1 warning emitted
rust 的这种方式不同于浅拷贝,因为在浅拷贝的同时让被拷贝者失效了 ,因此使用新的术语:移动(Move)
隐含的设计原则 : rust 不会自动创建数据的深拷贝
因为就运行性能而言,任何自动赋值的操作都是廉价的。
部分 move 当解构一个变量时,可以同时使用 move 和引用模式绑定的方式。当这么做时,部分 move 就会发生:变量中一部分的所有权被转移给其它变量,而另一部分我们获取了它的引用。
在这种情况下,原变量将无法再被使用,但是它没有转移所有权的那一部分依然可以使用 ,也就是之前被引用的那部分。
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 fn main () { #[derive(Debug)] struct Person { name: String , age: Box <u8 >, } let person = Person { name: String ::from ("Alice" ), age: Box ::new (20 ), }; let Person { name, ref age } = person; println! ("The person's age is {}" , age); println! ("The person's name is {}" , name); println! ("The person's age from person struct is {}" , person.age); }
clone 如果相对 heap 的数据进行深拷贝,而不仅仅时 stack 上面的数据,可以使用 clone 方法
1 2 3 4 5 6 7 8 9 10 11 12 let s1 =String ::from ("Hello" );let s2 =s1.clone ();println! ("{},{}" ,s1,s2);#[allow(unused)] fn main () { let t = (String ::from ("hello" ), String ::from ("world" )); let (s1, s2) = t.clone (); println! ("{:?}, {:?}, {:?}" , s1, s2, t); }
copy Copy trait ,可以用于像整数这样完全放在 stack 上面的类型
如果一个类型实现了 Copy trait,那么旧的变量在赋值后仍然可用
如果一个类型或该类型的一部分实现了 Drop trait,那么 Rust 不允许让它再去实现 Copy trait 了
任何简单标量及其组合类型都是 Copy 的
任何需要分配内存或某种资源的都不是 Copy 的
一些拥有 Copy trait 的类型:
所有整数类型,比如 u32。
布尔类型,bool,它的值是 true 和 false。
所有浮点数类型,比如 f64。
字符类型,char。
元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。
所有权与函数 将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或者复制,就像赋值语句一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 fn main () { let s = String ::from ("hello" ); takes_ownership (s); let x = 5 ; makes_copy (x); } fn takes_ownership (some_string: String ) { println! ("{}" , some_string); } fn makes_copy (some_integer: i32 ) { println! ("{}" , some_integer); }
当尝试在调用 takes_ownership 后使用 s 时,Rust 会抛出一个编译时错误。
返回值与作用域 返回值也可以转移所有权
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 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 ("yours" ); some_string } fn takes_and_gives_back (a_string: String ) -> String { a_string }
变量的所有权总是遵循相同的模式:
将值赋给另一个变量时移动它 。
当持有堆中数据值的变量离开作用域 时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
如果让函数获得所得值而不获得所有权,需要把传入的参数返回
1 2 3 4 5 6 7 8 9 10 11 12 13 fn 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) }
这样过于麻烦,Rust 对此提供了一个不用获取所有权就可以使用值的功能,叫做 引用 (references )。
引用与借用 1 2 3 4 5 6 7 8 9 10 11 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 () }
引用 (reference )像一个指针,因为它是一个地址 ,我们可以由此访问储存于该地址的属于其他变量的数据。
与指针不同,引用确保指向某个特定类型的有效值。
1 2 3 4 5 6 7 fn main () { let x = 5 ; let p = &x; println! ("x 的内存地址是 {:p}" , p); }
注意:与使用 & 引用相反的操作是 解引用 (dereferencing ),它使用解引用运算符 “ * “
1 2 3 let s1 = String ::from ("hello" ); let len = calculate_length (&s1);
&s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃 。
我们将创建一个引用的行为称为 借用 (borrowing )
正如变量默认是不可变的,引用也一样。引用(默认)不允许修改引用的值。
rust 会在某些情况下自动解引用 1 2 3 4 5 6 7 fn main () { let mut s = String ::from ("hello, " ); let p = &mut s; p.push_str ("world" ); }
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fn main () { let mut s = String ::from ("hello, " ); borrow_object (&s) } fn borrow_object (s: &String ) {}fn main () { let mut s = String ::from ("hello, " ); push_str (&mut s) } fn push_str (s: &mut String ) { s.push_str ("world" ) }
可变引用 1 2 3 4 5 6 7 8 9 fn main () { let mut s = String ::from ("hello" ); change (&mut s); } fn change (some_string: &mut String ) { some_string.push_str (", world" ); }
可变引用有一个很大的限制:某一时刻只能存在一个可变引用
1 2 3 4 5 6 let mut s = String ::from ("hello" ); let r1 = &mut s; let r2 = &mut s; println! ("{}, {}" , r1, r2);
这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争 (data race )类似于竞态条件,它可由这三个行为造成:
两个或更多指针同时访问同一数据。
至少有一个指针被用来写入数据。
没有同步数据访问的机制。
可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同时 拥有:
1 2 3 4 5 6 7 let mut s = String ::from ("hello" );{ let r1 = &mut s; } let r2 = &mut s;
另外一个限制:不可以同时拥有一个可变引用和一个不可变的引用
但是多个不可变的引用是可以的
1 2 3 4 5 6 7 let mut s = String ::from ("hello" );let r1 = &s; let r2 = &s; let r3 = &mut s; println! ("{}, {}, and {}" , r1, r2, r3);
悬空引用(悬垂引用 Dangling References) 在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
让我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免:
文件名: src/main.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 fn main () { let reference_to_nothing = dangle (); } fn dangle () -> &String { let s = String ::from ("hello" ); &s } Compiling loop_test v0.1.0 (C:\Users\cauchy\Desktop\rust\loop_test) error[E0106]: missing lifetime specifier --> src\main.rs:5 :16 | 5 | fn dangle () -> &String { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static ` lifetime | 5 | fn dangle () -> &'static String { | +++++++ For more information about this error, try `rustc --explain E0106`. error: could not compile `loop_test` due to previous error
ref ref 与 & 类似,可以用来获取一个值的引用,但是它们的用法有所不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn main () { let c = '中' ; let r1 = &c; let ref r2 = c; assert_eq! (*r1, *r2); assert_eq! (get_addr (r1),get_addr (r2)); } fn get_addr (r: &char ) -> String { format! ("{:p}" , r) }
引用规则(借用规则)总结
在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
引用必须总是有效的。
Ok: 从可变对象借用不可变
1 2 3 4 5 6 7 8 9 fn main () { let mut s = String ::from ("hello, " ); borrow_object (&s); s.push_str ("world" ); } fn borrow_object (s: &String ) {}
None Lexical Lifetimes(NLL) 非词法作用域生命周期
例子
1 2 3 4 5 6 7 8 9 10 11 fn main () { let mut s = String ::from ("hello, " ); let r1 = &mut s; r1.push_str ("world" ); let r2 = &mut s; r2.push_str ("!" ); println! ("{}" ,r1); }
注释掉 println 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn main () { let mut s = String ::from ("hello, " ); let r1 = &mut s; r1.push_str ("world" ); let r2 = &mut s; r2.push_str ("!" ); } fn main () { let mut x = 22 ; let p = &mut x; println! ("{}" , x); }
这段代码顺利编译,因为编译器知道 x 的可变借用并没有持续到作用域结尾,而是在 x 被再次使用之前就结束了,所以这里不存在冲突。
1 2 3 4 5 6 7 8 9 10 fn main() { let mut s = String::from("hello, "); let r1 = &mut s; let r2 = &mut s; // 在下面增加一行代码人为制造编译错误:cannot borrow `s` as mutable more than once at a time // 你不能同时使用 r1 和 r2 }
加入 r1.push_str(“world”);即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 warning: unused variable: `r2` --> src/main.rs:5:9 | 5 | let r2 = &mut s; | ^^ help : if this is intentional, prefix it with an underscore: `_r2` | = note: `#[warn(unused_variables)]` on by default error[E0499]: cannot borrow `s` as mutable more than once at a time --> src/main.rs:5:14 | 4 | let r1 = &mut s; | ------ first mutable borrow occurs here 5 | let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here ... 9 | r1.push_str("world" ); | -- first borrow later used here For more information about this error, try `rustc --explain E0499`. warning: `rust_programming` (bin "rust_programming" ) generated 1 warning error: could not compile `rust_programming` (bin "rust_programming" ) due to 1 previous error; 1 warning emitted
切片类型 Slice slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一类引用,所以它没有所有权。
1 2 3 4 5 6 7 8 9 10 fn first_word (s: &String ) -> usize { let bytes = s.as_bytes (); for (i, &item) in bytes.iter ().enumerate () { if item == b' ' { return i; } } s.len () }
该函数接收一个用空格分隔单词的字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则整个字符串就是一个单词,所以应该返回整个字符串。
first_word 函数有一个参数 &String。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 部分 字符串的办法。不过,我们可以返回单词结尾的索引,结尾由一个空格表示
因为需要逐个元素的检查 String 中的值是否为空格,需要用 as_bytes 方法将 String 转化为字节数组:
let bytes = s.as_bytes();
接下来,使用 iter 方法在字节数组上创建一个迭代器:
for (i, &item) in bytes.iter().enumerate() {
因为 enumerate 方法返回一个元组,我们可以使用模式来解构,所以在 for 循环中,我们指定了一个模式,其中元组中的 i 是索引而元组中的 &item 是单个字节。因为我们从 .iter().enumerate() 中获取了集合元素的引用,所以模式中使用了 &。
不过这有一个问题。我们返回了一个独立的 usize,不过它只在 &String 的上下文中才是一个有意义的数字。换句话说,因为它是一个与 String 相分离的值,无法保证将来它仍然有效。
字符串切片 string slice 字符串 slice (string slice )是 String 中一部分值的引用,它看起来像这样:
1 2 3 4 let s = String::from("hello world" ); let hello = &s[0..5]; let world = &s[6..11];
[开始索引..终止索引]
[starting_index..ending_index]
其中 starting_index 是 slice 的第一个位置,ending_index 则是 slice 最后一个位置的后一个值。
如果想要从索引 0 开始,可以不写两个点号之前的值
1 2 3 4 let s = String ::from ("hello" );let slice = &s[0 ..2 ];let slice = &s[..2 ];
如果 slice 包含 String 的最后一个字节,也可以舍弃尾部的数字
1 2 3 4 5 6 let s = String ::from ("hello" );let len = s.len ();let slice = &s[3 ..len];let slice = &s[3 ..];
也可以同时舍弃这两个值来获取整个字符串的 slice
1 2 3 4 5 6 let s = String ::from ("hello" );let len = s.len ();let slice = &s[0 ..len];let slice = &s[..];
注意 :字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内, 如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出。
1 2 3 4 5 6 fn main () { let s = "你好,世界" ; let slice = &s[0 ..2 ]; println! ("{}" ,slice); assert! (slice == "你" ); }
1 2 3 thread 'main' panicked at src/main.rs:3:19: byte index 2 is not a char boundary; it is inside '你' (bytes 0..3) of `你好,世界` note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
rust 中 String 索引操作 s[i] 不直接返回字符,因为 UTF-8 是可变长度编码。需要使用 chars() 或 bytes() 来遍历或操作字符串内容。比如 chars() 可以迭代 Unicode 字符,而bytes()单个字节。
重写函数,返回一个 slice(字符串切片返回值可以写成:&str)
1 2 3 4 5 6 7 8 9 10 11 fn first_word (s: &String ) -> &str { let bytes = s.as_bytes (); for (i, &item) in bytes.iter ().enumerate () { if item == b' ' { return &s[0 ..i]; } } &s[..] }
调用
1 2 3 4 5 6 7 8 9 fn main () { let mut s = String ::from ("hello world" ); let word = first_word (&s); s.clear (); println! ("the first word is: {}" , word); }
当拥有某值的不可变引用时,就不能再获取一个可变引用。因为 clear 需要清空 String,它尝试获取一个可变引用。在调用 clear 之后的 println! 使用了 word 中的引用,所以这个不可变的引用在此时必须仍然有效。Rust 不允许 clear 中的可变引用和 word 中的不可变引用同时存在,因此编译失败
字符串字面值就是 slice 1 let s = "Hello, world!" ;
这里 s 的类型是 &str:它是一个指向二进制程序特定位置的 slice, 这也就是为什么字符串字面值是不可变的;&str 是一个不可变引用。
字符串 slice 作为参数 在知道了能够获取字面值和 String 的 slice 后,我们对 first_word 做了改进,这是它的签名:
fn first_word(s: &String) -> &str {
而更有经验的 Rustacean 会编写出如下的签名,因为它使得可以对 &String 值和 &str 值使用相同的函数:
fn first_word(s: &str) -> &str {
如果有一个字符串 slice,可以直接传递它。如果有一个 String,则可以传递整个 String 的 slice 或对 String 的引用。这种灵活性利用了 deref coercions 的优势,定义一个获取字符串 slice 而不是 String 引用的函数使得我们的 API 更加通用并且不会丢失任何功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn main () { let my_string = String ::from ("hello world" ); let word = first_word (&my_string[0 ..6 ]); let word = first_word (&my_string[..]); let word = first_word (&my_string); let my_string_literal = "hello world" ; let word = first_word (&my_string_literal[0 ..6 ]); let word = first_word (&my_string_literal[..]); let word = first_word (my_string_literal); }
&String 可以被隐式地转换为&str 类型
1 2 3 4 5 6 7 8 9 10 11 12 13 fn main () { let mut s = String ::from ("hello world" ); let ch = first_character (&s); println! ("the first character is: {}" , ch); s.clear (); } fn first_character (s: &str ) -> &str { &s[..1 ] }
其他类型的 slice 字符串 slice,是针对字符串的。不过也有更通用的 slice 类型。考虑一下这个数组:
let a = [1, 2, 3, 4, 5];
就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分。我们可以这样做:
1 2 3 4 5 let a = [1 , 2 , 3 , 4 , 5 ];let slice = &a[1 ..3 ];assert_eq! (slice, &[2 , 3 ]);
这个 slice 的类型是 &[i32]。它跟字符串 slice 的工作方式一样,通过存储第一个集合元素的引用和一个集合总长度。你可以对其他所有集合使用这类 slice。
切片跟数组相似,但是切片的长度无法在编译期得知 ,因此你无法直接使用切片类型 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn main () { let arr = [1 , 2 , 3 ]; let s1 : [i32 ] = arr[0 ..2 ]; let s2 : str = "hello, world" as str ; } fn main () { let arr = [1 , 2 , 3 ]; let s1 : &[i32 ] = &arr[0 ..2 ]; let s2 : &str = "hello, world" as &str ; }
一个切片引用占用了2 个字 大小的内存空间( 从现在开始,为了简洁性考虑,如无特殊原因,我们统一使用切片来特指切片引用 )。 该切片的第一个字是指向数据的指针,第二个字是切片的长度 。
字的大小取决于处理器架构,例如在 x86-64 上,字的大小是 64 位也就是 8 个字节,那么一个切片引用就是 16 个字节大小。
1 2 3 4 5 6 7 fn main () { let arr: [char; 3] = ['中' , '国' , '人' ]; let slice = &arr[..2]; assert!(std::mem::size_of_val(&slice) == 16); }
切片( 引用 )可以用来借用数组的某个连续的部分 ,对应的签名是 &[T],可以与数组的签名对比下 [T; Length]。
1 2 3 4 5 6 fn main () { let arr: [i32; 5] = [1, 2, 3, 4, 5]; let slice: &[i32] = &arr[1..4]; assert_eq!(slice, &[2, 3, 4]); }
结构体 定义结构体 需要使用 struct 关键字并为整个结构体提供一个名字。
在大括号中,定义每一部分数据的名字和类型,我们称为 字段 (field )
1 2 3 4 5 6 struct User { active: bool , username: String , email: String , sign_in_count: u64 , }
实例化 1 2 3 4 5 6 7 8 fn main () { let user1 = User { email: String ::from ("someone@example.com" ), username: String ::from ("someusername123" ), active: true , sign_in_count: 1 , }; }
你可以在实例化一个结构体时将它整体标记为可变的,但是 Rust 不允许我们将结构体的某个字段专门指定为可变的.
1 2 3 4 5 6 7 8 9 10 11 12 13 struct Person { name: String , age: u8 , } fn main () { let age = 18 ; let mut p = Person { name: String ::from ("sunface" ), age, }; p.age = 30 ; p.name = String ::from ("sunfei" ); }
访问 1 2 3 4 5 6 7 8 9 10 fn main () { 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" ); }
一旦 struct 的实例是可变的,那么实例中所有的字段都是可变的
字段初始化简写 当字段名与字段值对应的变量名相同时,就可以使用字段初始化简写的方式
1 2 3 4 5 6 7 8 fn build_user (email: String , username: String ) -> User { User { email, username, active: true , sign_in_count: 1 , } }
Struct 更新语法 基于 现有的 struct 实例创建一个新的实例
1 2 3 4 5 6 7 8 9 10 fn main () { let user2 = User { active: user1.active, username: user1.username, email: String ::from ("another@example.com" ), sign_in_count: user1.sign_in_count, }; }
使用 struct 更新语法
1 2 3 4 5 6 7 8 fn main () { let user2 = User { email: String ::from ("another@example.com" ), ..user1 }; }
Tuple Struct 元组结构体 元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。
适用于给整个元组取一个名字,并使元组成为与其他元组不同的类型时
1 2 3 4 5 6 7 struct Color (i32 , i32 , i32 );struct Point (i32 , i32 , i32 );fn main () { let black = Color (0 , 0 , 0 ); let origin = Point (0 , 0 , 0 ); }
访问此类结构体,与访问元组相同:
1 2 3 4 5 6 struct Point (i32 , i32 );fn main () { let p = Point (10 , 20 ); println! ("x = {}, y = {}" , p.0 , p.1 ); }
没有任何字段的类单元结构体(unit-like structs) 没有任何字段的类单元结构体 ,它们类似于 ()
1 2 3 4 5 struct AlwaysEqual ;fn main () { let subject = AlwaysEqual; }
struct 中的所有权 在示例 5-1 中的 User 结构体的定义中,我们使用了自身拥有所有权的 String 类型而不是 &str 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据 ,为此只要整个结构体是有效的话其数据也是有效的。
可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 生命周期 (lifetimes )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct User { active: bool , username: &str , email: &str , sign_in_count: u64 , } fn main () { let user1 = User { email: "someone@example.com" , username: "someusername123" , active: true , sign_in_count: 1 , }; }
报错:缺少生命周期标识符
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 error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | username: &str, | ^ expected named lifetime parameter | help : consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 ~ username: &' a str, | error[E0106]: missing lifetime specifier --> src/main.rs:4:12 | 4 | email: &str, | ^ expected named lifetime parameter | help : consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 | username: &str, 4 ~ email: &' a str, | For more information about this error, try `rustc --explain E0106`. error: could not compile `rust_programming` (bin "rust_programming" ) due to 2 previous errors
打印 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 }
struct 的方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #[derive(Debug)] struct Rectangle { width: u32 , length: u32 , } impl Rectangle { fn area (&self )-> u32 { self .width*self .length } } fn main () { let rect =Rectangle{ width:30 , length:50 , }; println! ("{}" ,rect.area ()); println! ("{:#?}" ,rect) }
在 impl 块里面定义方法
方法的第一个参数可以是&self,也可以获得其所有权或可变借用,和其他参数一样
更良好的代码组织
方法调用的运算符 在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 object 是一个指针,那么 object->something() 就像 (*object).something() 一样。
Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用 (automatic referencing and dereferencing )的功能。方法调用 是 Rust 中少数几个拥有这种行为的地方 。
它是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:
1 2 p1.distance(&p2); (&p1).distance(&p2);
这种自动引用的行为之所以有效,是因为方法有一个明确的接收者 ———— self 的类型 。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。
关联函数 1 2 3 4 5 6 7 8 impl Rectangle { fn square (size: u32 ) -> Self { Self { width: size, height: size, } } }
所有在 impl 块中定义的函数被称为 关联函数 (associated functions )
不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。这些函数的名称通常为 new ,但 new 并不是一个关键字 。
使用结构体名和 :: 语法来调用这个关联函数:比如 let sq = Rectangle::square(3) ;。这个函数位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间
每个结构体都允许拥有多个 impl 块。
示例 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 struct Point { x: f64 , y: f64 , } impl Point { fn origin () -> Point { Point { x: 0.0 , y: 0.0 } } fn new (x: f64 , y: f64 ) -> Point { Point { x: x, y: y } } } struct Rectangle { p1: Point, p2: Point, } impl Rectangle { fn area (&self ) -> f64 { let Point { x: x1, y: y1 } = self .p1; let Point { x: x2, y: y2 } = self .p2; ((x1 - x2) * (y1 - y2)).abs () } fn perimeter (&self ) -> f64 { let Point { x: x1, y: y1 } = self .p1; let Point { x: x2, y: y2 } = self .p2; 2.0 * ((x1 - x2).abs () + (y1 - y2).abs ()) } fn translate (&mut self , x: f64 , y: f64 ) { self .p1.x += x; self .p2.x += x; self .p1.y += y; self .p2.y += y; } } struct Pair (Box <i32 >, Box <i32 >);impl Pair { fn destroy (self ) { let Pair (first, second) = self ; println! ("Destroying Pair({}, {})" , first, second); } } fn main () { let rectangle = Rectangle { p1: Point::origin (), p2: Point::new (3.0 , 4.0 ), }; println! ("Rectangle perimeter: {}" , rectangle.perimeter ()); println! ("Rectangle area: {}" , rectangle.area ()); let mut square = Rectangle { p1: Point::origin (), p2: Point::new (1.0 , 1.0 ), }; square.translate (1.0 , 1.0 ); let pair = Pair (Box ::new (1 ), Box ::new (2 )); pair.destroy (); }
枚举 定义枚举 1 2 3 4 enum IpAddrKind { V4, V6, }
通过在代码中定义一个 IpAddrKind 枚举来表现这个概念并列出可能的 IP 地址类型,V4 和 V6。这被称为枚举的 成员 (variants ):
在创建枚举时,你可以使用显式的整数 设定枚举成员的值。
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 enum Number { Zero, One, Two, } enum Number1 { Zero = 0 , One, Two, } enum Number2 { Zero = 0 , One = 1 , Two = 2 , } fn main () { assert_eq! (Number::One as u8 , Number1::One as u8 ); assert_eq! (Number1::One as u8 , Number2::One as u8 ); }
枚举值 可以像这样创建 IpAddrKind 两个不同成员的实例:
1 2 let four = IpAddrKind::V4; let six = IpAddrKind::V6;
枚举成员中的值可以使用模式匹配来获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 enum Message { Quit, Move { x: i32 , y: i32 }, Write (String ), ChangeColor (i32 , i32 , i32 ), } fn main () { let msg = Message::Move{x: 1 , y: 1 }; if let Message ::Move{x:a,y: b} = msg { assert_eq! (a, b); } else { panic! ("不要让这行代码运行!" ); } }
将数据附加到枚举的变体中 使用 struct
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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" ), };
仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。IpAddr 枚举的新定义表明了 V4 和 V6 成员都关联了 String 值:
1 2 3 4 5 6 7 8 enum IpAddr { V4 (String ), V6 (String ), } let home = IpAddr::V4 (String ::from ("127.0.0.1" )); let loopback = IpAddr::V6 (String ::from ("::1" ));
我们直接将数据附加到枚举的每个成员上 ,这样就不需要一个额外的结构体了。
1 2 3 4 5 6 7 8 enum IpAddr { V4 (u8 , u8 , u8 , u8 ), V6 (String ), } let home = IpAddr::V4 (127 , 0 , 0 , 1 ); let loopback = IpAddr::V6 (String ::from ("::1" ));
注意虽然标准库中包含一个 IpAddr 的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。
枚举可以嵌入多种类型
1 2 3 4 5 6 enum Message { Quit, Move { x: i32 , y: i32 }, Write (String ), ChangeColor (i32 , i32 , i32 ), }
Quit 没有关联任何数据。
Move 类似结构体包含命名字段。
Write 包含单独一个 String。
ChangeColor 包含三个 i32。
枚举中定义函数 结构体和枚举还有另一个相似点:就像可以使用 impl 来为结构体定义方法那样,也可以在枚举上定义方法 。这是一个定义于我们 Message 枚举上的叫做 call 的方法:
1 2 3 4 5 6 7 8 impl Message { fn call (&self ) { } } let m = Message::Write (String ::from ("hello" )); m.call ();
方法体使用了 self 来获取调用方法的值。这个例子中,创建了一个值为 Message::Write(String::from(“hello”)) 的变量 m,而且这就是当 m.call() 运行时 call 方法中的 self 的值。
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 #[derive(Debug)] enum TrafficLightColor { Red, Yellow, Green, } impl TrafficLightColor { fn color (&self ) -> String { match *self { TrafficLightColor::Red => "red" .to_string (), TrafficLightColor::Yellow => "yellow" .to_string (), TrafficLightColor::Green => "green" .to_string (), } } } fn main () { let c = TrafficLightColor::Yellow; assert_eq! (c.color (), "yellow" ); println! ("{:?}" , c); }
Option 枚举 定义于标准库中,在 prelude(预导入模块中)
Rust 没有 Null ,提供了类似于 Null 概念的枚举-Option,它定义于标准库中
1 2 3 4 enum Option <T> { None , Some (T), }
使用
1 2 3 4 let some_number = Some (5 );let some_char = Some ('e' );let absent_number : Option <i32 > = None ;
当有一个 Some 值时,我们就知道存在一个值,而这个值保存在 Some 中。当有个 None 值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,Option 为什么就比空值要好呢?
简而言之,因为 Option 和 T(这里 T 可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option。例如,这段代码不能编译,因为它尝试将 Option 与 i8 相加:
1 2 3 4 let x : i8 = 5 ; let y : Option <i8 > = Some (5 ); let sum = x + y;
事实上,错误信息意味着 Rust 不知道该如何将 Option 与 i8 相加,因为它们的类型不同。当在 Rust 中拥有一个像 i8 这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 Option(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。
换句话说,在对 Option 进行 T 的运算之前必须将其转换为 T。
为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 Option 类型,你就 可以 安全的认定它的值不为空。
这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥 以增加 Rust 代码的安全性。
枚举实现链表 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #![allow(unused)] enum List { Cons (u32 , Box <List>), Nil, } impl List { fn new () -> List { Nil } fn prepend (self , elem: u32 ) -> List { Cons (elem, Box ::new (self )) } fn len (&self ) -> u32 { match *self { Cons (_,ref tail) => 1 + tail.len (), Nil => 0 } } fn stringify (&self ) -> String { match *self { Cons (head, ref tail) => { format! ("{}, {}" , head, tail.stringify ()) }, Nil => { format! ("Nil" ) }, } } } fn main () { let mut list = List::new (); list = list.prepend (1 ); list = list.prepend (2 ); list = list.prepend (3 ); println! ("链表的长度是: {}" , list.len ()); println! ("{}" , list.stringify ()); }
模式匹配 match 控制流结构 Rust 有一个叫做 match 的极为强大的控制流运算符,它允许我们将一个值 与一系列的模式 相比较,并根据相匹配的模式执行相应代码
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 enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents (coin: Coin) -> u8 { match coin { Coin::Penny => 1 , Coin::Nickel => 5 , Coin::Dime => 10 , Coin::Quarter => 25 , } } fn value_in_cents (coin: Coin) -> u8 { match coin { Coin::Penny => { println! ("Lucky penny!" ); 1 } Coin::Nickel => 5 , Coin::Dime => 10 , Coin::Quarter => 25 , } }
matches! matches!看起来像 match, 但是它可以做一些特别的事情
1 2 3 4 5 6 7 8 fn main () { let alphabets = ['a' , 'E' , 'Z' , '0' , 'x' , '9' , 'Y' ]; for ab in alphabets { assert! (matches!(ab, 'a' ..='z' | 'A' ..='Z' | '0' ..='9' )) } }
下面的代码会报错,原因是枚举默认没有实现 PartialEq,所以不能用==比较
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 32 33 34 35 36 37 38 enum MyEnum { Foo, Bar } fn main () { let mut count = 0 ; let v = vec! [MyEnum::Foo,MyEnum::Bar,MyEnum::Foo]; for e in v { if e == MyEnum::Foo { count += 1 ; } } assert_eq! (count, 2 ); } Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo) error[E0369]: binary operation `==` cannot be applied to type `MyEnum` --> src\main.rs:13 :14 | 13 | if e == MyEnum::Foo { | - ^^ ----------- MyEnum | | | MyEnum | note: an implementation of `PartialEq <_>` might be missing for `MyEnum` --> src\main.rs:3 :1 | 3 | enum MyEnum { | ^^^^^^^^^^^ must implement `PartialEq <_>` help: consider annotating `MyEnum` with `#[derive(PartialEq)] ` | 3 | #[derive(PartialEq)] | For more information about this error, try `rustc --explain E0369`. error: could not compile `demo` due to previous error
修改为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 enum MyEnum { Foo, Bar } fn main () { let mut count = 0 ; let v = vec! [MyEnum::Foo,MyEnum::Bar,MyEnum::Foo]; for e in v { if matches!(e, MyEnum::Foo) { count += 1 ; } } assert_eq! (count, 2 ); }
绑定值的模式 匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值的。
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 => 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)); }
匹配 Option 1 2 3 4 5 6 7 8 9 10 11 fn main (){ let five = Some (5 ); let six = plus_one (five); let none = plus_one (None ); } fn plus_one (x: Option <i32 >) -> Option <i32 > { match x { None => None , Some (i) => Some (i + 1 ), } }
match 匹配必须穷举所有的可能性 使用_占位符 (必须放到最后面)
1 2 3 4 5 match dice_roll { 3 => add_fancy_hat (), 7 => remove_fancy_hat (), _ => reroll (), }
if let 简洁控制流 处理只关心一种模式匹配而忽略其它匹配的情况
1 2 3 4 5 let config_max = Some (3u8 );match config_max { Some (max) => println! ("The maximum is configured to be {}" , max), _ => (), }
使用 if let
1 2 3 4 let config_max = Some (3u8 ); if let Some (max) = config_max { println! ("The maximum is configured to be {}" , max); }
在这个例子中,模式是 Some(max),max 绑定为 Some 中的值。接着可以在 if let 代码块中使用 max 了,就跟在对应的 match 分支中一样。模式不匹配时 if let 块中的代码不会执行。
放弃了穷举的可能性
可以把 if let 看作是 match 的语法糖
搭配 else 使用
1 2 3 4 5 6 let mut count = 0 ; if let Coin ::Quarter (state) = coin { println! ("State quarter from {:?}!" , state); } else { count += 1 ; }
模式匹配中的变量遮蔽 1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn main () { let age = Some (30 ); if let Some (age) = age { assert_eq! (age, 30 ); } match age { Some (age) => println! ("age 是一个新的变量,它的值是 {}" ,age), _ => () } }
Workspace, Package,Crate,Module
包 (Packages ): Cargo 的一个功能,它允许你构建、测试和分享 crate。
Crate :一个模块的树形结构,它形成了库或二进制项目。
模块 (Modules )和 use : 允许你控制作用域和路径的私有性。
路径 (path ):一个命名例如结构体、函数或模块等项的方式
层级
名称
说明
示例
📦 Package
“包”,Cargo 管理的单位(一个项目)
包含一个或多个 crate,以及 Cargo.toml
整个项目目录
🧩 Crate
“单个编译单元”,可以是库或可执行程序
每次 rustc 编译的就是一个 crate
src/lib.rs 或 src/main.rs
📁 Module
模块,组织 crate 内部代码的结构
类似 C++ 的命名空间或 Python 的模块
mod network;
🛣️ Path
用来访问模块、结构体、函数的路径
类似 std::io::Write
crate::foo::bar()
Crate 的类型
每个 crate 是一个独立的编译单元。 Rust 编译器一次只编译一个 crate。
Crate Root
是源代码文件,Rust 编译器从这里开始,组成 Crate 的根 Module。 Crate Root 是 Rust 编译器构建 crate 时的起点文件 , 它决定了 模块树(module tree) 的最顶层结构。
举个例子
假设我们有一个最简单的项目:
1 2 3 4 5 my_project/ ├── Cargo.toml └── src / ├── main .rs └── lib.rs
这两个文件都是 可能的 crate root 。
文件
crate 类型
作用
src/main.rs
binary crate root
程序入口点(必须包含 fn main())
src/lib.rs
library crate root
库的入口点(定义库的模块结构)
编译器的工作逻辑
当运行cargo build时:
Cargo 会:
读取 Cargo.toml;
找到当前 package 里的 crate roots (例如 src/main.rs、src/lib.rs);
对每个 crate root 调用编译器,从这个文件开始构建整个 crate 的模块树。
一个 Package
包含 1 个 Cargo.toml,它描述了如何构建这些 Crates
只能包含 0-1 个 library crate
可以包含任意数量的 binary crate
但必须至少包含一个 crate (library 或者 binary)
举例:
1 2 3 4 5 6 7 8 my_package/ ├── Cargo.toml ├── src/ │ ├── lib.rs │ ├── main.rs │ └── bin/ │ ├── tool1.rs │ └── tool2.rs
在 Cargo.toml 里无需特别配置。运行:
1 2 3 4 5 6 cargo run cargo run --bin tool1 cargo run --bin tool1
workspace
workspace 是多个 package 的集合,每个 package 都可以有自己的 library crate。如下例子:
1 2 3 4 5 6 7 8 9 10 11 12 my_workspace/ ├── Cargo.toml ├── app/ │ ├── Cargo.toml │ └── src/main.rs ├── utils/ │ ├── Cargo.toml │ └── src/lib.rs └── network/ ├── Cargo.toml └── src/lib.rs
workspace 声明的 toml
1 2 [workspace] members = ["app" , "utils" , "network" ]
Cargo 的惯例 src/main.rs
binary crate 的 crate root
crate 名与 package 名相同
src/lib.rs
package 包含一个 Library crate
library crate 的 crate root
crate 名与 package 名相同
一个 package 可以同时包含 src/main.rs 和 src/lib.rs
一个 Package 可以有多个 binary crate:
文件放在 src/bin 下,每个文件都是单独的 binary crate
定义 module 来控制作用域和私有性 Module
在一个 crate 内,将 crate 进行分组
控制项目(item) 的私有性,public,private
建立 module
mod 关键字
可嵌套
可包含其他项的定义(struct,enum,常量,trait,函数等)的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 () {} } }
上述代码的模块树
1 2 3 4 5 6 7 8 9 crate └── front_of_house ├── hosting │ ├── add_to_waitlist │ └── seat_at_table └── serving ├── take_order ├── serve_order └── take_payment
src/main.rs 和 src/lib.rs 叫做 crate roots
这两个文件(任意一个)的内容形成了名为 crate 的模块,位于整个模块树的根部
整个模块树在隐式的 crate 模块下
路径 为了在 Rust 的模块中找到某个条目,需要使用路径
绝对路径 从 crate root 开始,使用 crate 名或字面值 crate
相对路径 从当前模块开始,使用 self,super 或当前模块的标识符
路径至少由一个标识符组成,标识符之间使用::
src/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 mod front_of_house { mod hosting { fn add_to_waitlist () {} } } pub fn eat_at_restaurant () { crate::front_of_house::hosting::add_to_waitlist (); front_of_house::hosting::add_to_waitlist (); }
私有边界(privacy boundary)
Rust 的所有条目(函数,方法,struct,enum,模块,常量) 默认都是私有的
父级模块无法访问所有子模块的私有条目
子模块里可以使用所有 祖先模块中的条目
同级模块 可以互相调用
pub 关键字可以标记为公共的
super super:用来访问父级模块路径中的内容,类似于文件系统中的..
1 2 3 4 5 6 7 8 9 10 fn serve_order () {}mod back_of_house { fn fix_incorrect_order () { cook_order (); super::serve_order (); } fn cook_order () {} }
pub struct pub 放在 struct 前 :
struct 是公共的
struct 的字段默认是私有的 ,字段前面加 pub 就可以设为公有的
文件名: src/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 26 27 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 前:
enum 是公共的
enum 的变体默认也都是公共的(不需要加 pub 关键字)
use 关键字 使用 use 关键字将路径引入作用域
1 2 3 4 5 6 7 8 9 10 11 12 13 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 的习惯用法
文件名: src/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 mod front_of_house { pub mod hosting { pub fn add_to_waitlist () {} } } use self::front_of_house::hosting;pub fn eat_at_restaurant () { hosting::add_to_waitlist (); hosting::add_to_waitlist (); hosting::add_to_waitlist (); }
struct,enum,其他:指定完整路径(指定到本身)
文件名: src/main.rs
1 2 3 4 5 6 use std::collections::HashMap;fn main () { let mut map = HashMap::new (); map.insert (1 , 2 ); }
两个具有相同名称的项带入作用域
文件名: src/lib.rs
1 2 3 4 5 6 7 8 9 10 use std::fmt;use std::io;fn function1 () -> fmt::Result { } fn function2 () -> io::Result <()> { }
使用 as 关键字提供新的名称 文件名: src/lib.rs
1 2 3 4 5 6 7 8 9 10 use std::fmt::Result ;use std::io::Result as IoResult;fn function1 () -> Result { } fn function2 () -> IoResult<()> { }
pub use 重导出 使用 use 将路径(名称)导入到作用域内后,该名称在此作用域内是私有的
如果你希望 外部模块也能通过你的路径访问 那个条目(比如函数、结构体、模块等),就可以用 pub use,re-export 重导出
pub use:重导出
将条目引入到作用域
该条目可以被外部代码 引入到它们的作用域
pub(in Crate) 有时我们希望某一个项只对特定的包可见,那么就可以使用 pub(in Crate) 语法.
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pub mod a { pub const I: i32 = 3 ; fn semisecret (x: i32 ) -> i32 { use self::b::c::J; x + J } pub fn bar (z: i32 ) -> i32 { semisecret (I) * z } pub fn foo (y: i32 ) -> i32 { semisecret (I) + y } mod b { pub (in crate::a) mod c { pub (in crate::a) const J: i32 = 4 ; } } }
使用外部包(package) 1.Cargo.toml 添加依赖的包
2.use 将特定条目引入到作用域
标准库 std 也被当做外部包,但是不需要修改 Cargo.toml 来包含 std
需要使用 use 将 std 中的特定条目引入当前作用域
使用嵌套路径清理大量的 use 语句 路径相同的部分::{路径差异的部分}
1 2 use std::{cmp::Ordering,io};fn main ()
如果两个 use 路径之一是另一个的子路径
使用 self
1 2 3 use std::io::{self ,Write}
通配符* 使用*可以把路径中所有的公共条目都引入到作用域
谨慎使用
应用场景:
prelude
测试,将所有被测试代码引入到 tests 模块
将模块拆分为不同的文件 模块定义时,如果模块名后面时”;”,而不是代码块
Rust 会从模块同名的文件中加载内容
模块树不会发生变化
示例:
文件名: src/lib.rs
1 2 3 4 5 6 7 8 9 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 (); }
示例 :声明 frontof_house 模块,其内容将位于 _src/front_of_house.rs
src/front_of_house.rs 会获取 front_of_house 模块的定义内容,如示例所示。
文件名: src/front_of_house.rs
1 2 3 pub mod hosting { pub fn add_to_waitlist () {} }
示例:
定义模块 front_of_house
方式一 :直接在文件中写模块内容
文件:src/front_of_house.rs
1 2 3 pub mod hosting { pub fn add_to_waitlist () {} }
这里 front_of_house 模块直接定义了子模块 hosting。
好处:简单,模块小的时候可以这样写。
方式二 :把子模块拆到单独文件
在 front_of_house.rs 中只声明子模块:
创建目录和文件结构:
1 2 3 4 5 src /├── lib.rs ├── front_of_house.rs ← front_of_house 模块的主体 └── front_of_house/ ← front_of_house 的子模块目录 └── hosting.rs ← 子模块 hosting 的实现
在 hosting.rs 中写实际内容:
1 pub fn add_to_waitlist () {}
这时 hosting 的内容完全放到 hosting.rs。
好处:模块大时,拆开文件更清晰、可维护。
传统风格
Rust Edition 2018 前,使用下面这种风格
1 2 3 4 src /└── front_of_house/ ├── mod.rs └── hosting.rs
常见集合 Vector Vec 叫做 vector
创建 vector Vec::new 函数
let v:Vec=Vec::new();
使用初始值创建 Vec,使用vec! 宏
let v = vec![1,2,3];
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let arr : [u8 ; 3 ] = [1 , 2 , 3 ]; let v = Vec ::from (arr); is_vec (v); let v = vec! [1 , 2 , 3 ]; is_vec (v); let v = vec! (1 , 2 , 3 ); is_vec (v); let v1 = vec! (arr);
添加元素 1 2 let mut v :Vec <i32 >=Vec ::new ();v.push (1 );
删除 Vector 类似于任何其他的 struct,vector 在其离开作用域时会被释放
1 2 3 4 5 { let v = vec! [1 , 2 , 3 , 4 ]; }
读取 Vector 中的值
1 2 3 4 5 6 7 8 9 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." ), }
使用索引方法访问超出数组元素的值时,程序会 panic
而使用 get 方法访问时程序会返回一个 None
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn main () { let mut v = Vec ::from ([1 , 2 , 3 ]); for i in 0 ..5 { println! ("{:?}" , v.get (i)) } for i in 0 ..5 { if let Some (x) = v.get (i) { v[i] = x + 1 } else { v.push (i + 2 ) } } assert_eq! (format! ("{:?}" ,v), format! ("{:?}" , vec! [2 , 3 , 4 , 5 , 6 ])); println! ("Success!" ) }
引用借用规则 当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,如果尝试在函数的后面引用这个元素是行不通的
1 2 3 4 5 6 7 let mut v = vec! [1 , 2 , 3 , 4 , 5 ]; let first = &v[0 ]; v.push (6 ); println! ("The first element is: {}" , first);
为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中 。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况 。
遍历 Vector 1 2 3 4 let v = vec! [100 , 32 , 57 ]; for i in &v { println! ("{}" , i); }
我们也可以遍历可变 vector 的每一个元素的可变引用以便能改变他们
1 2 3 4 let mut v = vec! [100 , 32 , 57 ]; for i in &mut v { *i += 50 ; }
为了修改可变引用所指向的值,在使用 += 运算符之前必须使用解引用运算符(*)获取 i 中的值。
扩展 Vector Vec 可以使用 extend 方法进行扩展
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn main () { let mut v1 = Vec ::from ([1 , 2 , 4 ]); v1.pop (); v1.push (3 ); let mut v2 = Vec ::new (); v2.extend ([1 , 2 , 3 ]); assert_eq! (format! ("{:?}" ,v1), format! ("{:?}" ,v2)); println! ("Success!" ) }
使用 enum 来使 Vec 存储多种数据类型 定义一个枚举,以便能在 vector 中存放不同类型的数据
1 2 3 4 5 6 7 8 9 10 11 enum SpreadsheetCell { Int (i32 ), Float (f64 ), Text (String ), } let row = vec! [ SpreadsheetCell::Int (3 ), SpreadsheetCell::Text (String ::from ("blue" )), SpreadsheetCell::Float (10.12 ), ];
使用特征对象来使 Vec 存储多种数据类型 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 trait IpAddr { fn display (&self ); } struct V4 (String );impl IpAddr for V4 { fn display (&self ) { println! ("ipv4: {:?}" ,self .0 ) } } struct V6 (String );impl IpAddr for V6 { fn display (&self ) { println! ("ipv6: {:?}" ,self .0 ) } } fn main () { let v : Vec <Box <dyn IpAddr>> = vec! [ Box ::new (V4 ("127.0.0.1" .to_string ())), Box ::new (V6 ("::1" .to_string ())), ]; for ip in v { ip.display (); } }
将 X 类型转换(From/Into 特征)成 Vec 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 fn main () { let arr = [1 , 2 , 3 ]; let v1 = Vec ::from (arr); let v2 : Vec <i32 > = arr.into (); assert_eq! (v1, v2); let s = "hello" .to_string (); let v1 : Vec <u8 > = s.into (); let s = "hello" .to_string (); let v2 = s.into_bytes (); assert_eq! (v1, v2); let s = "hello" ; let v3 = Vec ::from (s); assert_eq! (v2, v3); println! ("Success!" ) }
切片 与 String 的切片类似, Vec 也可以使用切片。如果说 Vec 是可变的,那它的切片就是不可变或者说只读的,我们可以通过 & 来获取切片。
在 Rust 中,将切片作为参数进行传递是更常见的使用方式 ,例如当一个函数只需要可读性时,那传递 Vec 或 String 的切片 &[T] / &str 会更加适合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 fn main () { let mut v = vec! [1 , 2 , 3 ]; let slice1 = &v[..]; let slice2 = &v[0 ..v.len ()]; assert_eq! (slice1, slice2); let vec_ref : &mut Vec <i32 > = &mut v; (*vec_ref).push (4 ); let slice3 = &mut v[0 ..]; assert_eq! (slice3, &[1 , 2 , 3 , 4 ]); println! ("Success!" ) }
容量 容量 capacity 是已经分配好的内存空间,用于存储未来添加到 Vec 中的元素。而长度 len 则是当前 Vec 中已经存储的元素数量。如果要添加新元素时,长度将要超过已有的容量,那容量会自动进行增长:Rust 会重新分配一块更大的内存空间,然后将之前的 Vec 拷贝过去,因此,这里就会发生新的内存分配
若这段代码会频繁发生,那频繁的内存分配会大幅影响我们系统的性能,最好的办法就是提前分配好足够的容量,尽量减少内存分配。
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 fn main () { let mut vec = Vec ::with_capacity (10 ); assert_eq! (vec.len (), 0 ); assert_eq! (vec.capacity (), 10 ); for i in 0 ..10 { vec.push (i); } assert_eq! (vec.len (), 10 ); assert_eq! (vec.capacity (), 10 ); vec.push (11 ); assert_eq! (vec.len (), 11 ); assert! (vec.capacity () >= 11 ); let mut vec = Vec ::with_capacity (100 ); for i in 0 ..100 { vec.push (i); } assert_eq! (vec.len (), 100 ); assert_eq! (vec.capacity (), 100 ); println! ("Success!" ) }
只要为 Vec 实现了 From 特征,那么 T 就可以被转换成 Vec。
String
字符串是 Byte 的集合
UFT-8 编码
一些方法能将 byte 解析为文本
字符串是什么? Rust 的核心语言层面,只有一个字符串类型:字符串切片 str(或者&str)
字符串切片:对存储在其它地方,UTF-8 编码的字符串引用
字符串的字面值:存储在二进制文件中,也是字符串切片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn main () { let s = String ::from ("hello, 世界" ); let slice1 = &s[0 ..1 ]; assert_eq! (slice1, "h" ); let slice2 = &s[7 ..10 ]; assert_eq! (slice2, "世" ); for (i, c) in s.chars ().enumerate () { if i == 7 { assert_eq! (c, '世' ) } } println! ("Success!" ) }
事实上 String 是一个智能指针 ,它作为一个结构体存储在栈上,然后指向存储在堆上的字符串底层数据。
存储在栈上的智能指针结构体由三部分组成:一个指针只指向堆上的字节数组,已使用的长度以及已分配的容量 capacity (已使用的长度小于等于已分配的容量,当容量不够时,会重新分配内存空间)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 use std::mem;fn main () { let story = String ::from ("Rust By Practice" ); let mut story = mem::ManuallyDrop::new (story); let ptr = story.as_mut_ptr (); let len = story.len (); let capacity = story.capacity (); assert_eq! (16 , len); let s = unsafe { String ::from_raw_parts (ptr, len, capacity) }; assert_eq! (*story, s); println! ("Success!" ) }
其它字符串类型 String 类型
来自标准库,也是 UTF-8 编码
String vs Str:拥有或借用的变体
其他字符串类型
OsString / OsStr
1 2 3 4 5 use std::ffi::OsString;fn main () { let mut os_string = OsString::from ("hello" ); os_string.push (" world" ); }
主要用于处理文件路径、命令行参数、环境变量等系统字符串。
CString / CStr
1 2 3 use std::ffi::CString;let c_string = CString::new ("hello" ).expect ("NUL found!" );
来自 std::ffi,用于 与 C 语言接口交互 。
特点 :
以 NUL 结尾 (\0) 的字符串。
CString 拥有数据,保证没有内部 NUL。
CStr 是不可变借用,类似于 &str。
CString 没有提供类似 push_str 或索引访问的方法修改内容,一旦创建,就不能修改内部字节 。
String 与&str 的转换 使用 to_string()
1 2 3 4 5 6 7 8 fn main () { let s = "hello, world" .to_string (); greetings (s) } fn greetings (s: String ) { println! ("{}" ,s) }
使用 String::from()
1 2 3 4 5 6 7 8 fn main () { let s = String ::from ("hello, world" ); greetings (s) } fn greetings (s: String ) { println! ("{}" ,s) }
字符串转义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn main () { let byte_escape = "I'm writing Ru\x73__!" ; println! ("What are you doing\x3F (\\x3F means ?) {}" , byte_escape); let unicode_codepoint = "\u{211D}" ; let character_name = "\"DOUBLE-STRUCK CAPITAL R\"" ; println! ("Unicode character {} (U+211D) is called {}" , unicode_codepoint, character_name ); let long_string = "String literals can span multiple lines. The linebreak and indentation here \ can be escaped too!" ; println! ("{}" , long_string); }
有时候需要转义的字符很多,我们会希望使用更方便的方式来书写字符串: raw string.
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 fn main () { let raw_str = r"Escapes don't work here: \x3F \u{211D}" ; println! ("{}" , raw_str); let quotes = r#"And then I said: "There is no escape!""# ; println! ("{}" , quotes); let longer_delimiter = r###"A string with "# in it. And even "##!"### ; println! ("{}" , longer_delimiter); } fn main () { let raw_str = "Escapes don't work here: \x3F \u{211D}" ; assert_eq! (raw_str, "Escapes don't work here: ? ℝ" ); let quotes = r#"And then I said: "There is no escape!""# ; println! ("{}" , quotes); let delimiter = r###"A string with "# in it. And even "##!"### ; println! ("{}" , delimiter); let long_delimiter = r###"Hello, "##""### ; assert_eq! (long_delimiter, "Hello, \"##\"" ) }
这里 r#”标记一个原始字符串的开始,”#标记一个字符串的结束,如果还是有歧义可以继续加#
创建一个新的 String String::new()
let mut s = String::new();
使用 to_string()
1 2 3 4 5 6 let data = "initial contents" ; let s = data.to_string (); let s = "initial contents" .to_string ();
也可以使用 string::from()
let s = String::from(“initial contents”);
更新 String push_str() 1 2 let mut s = String ::from ("foo" ); s.push_str ("bar" );
push_str()方法不会获得参数的所有权
1 2 3 4 5 6 7 8 9 let mut s1 = String ::from ("foo" ); let s2 = "bar" ; s1.push_str (s2); println! ("s2 is {}" , s2); let mut s1 = String ::from ("foo" ); let s2 = "bar" ; s1.push_str (&s2); println! ("s2 is {}" , s2);
push() push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中
1 2 let mut s = String ::from ("lo" ); s.push ('l' );
+连接字符串 只能将 String 跟 &str 类型进行拼接,并且 String 的所有权在此过程中会被 move
1 2 3 let s1 = String ::from ("Hello, " ); let s2 = String ::from ("world!" ); let s3 = s1 + &s2;
+ 运算符使用了 add 函数,这个函数签名看起来像这样:
1 fn add (self , s: &str ) -> String {
这并不是标准库中实际的签名;
但是&s2 的类型是 &String 而不是 &str。那么为什么还能编译呢
之所以能够在 add 调用中使用 &s2 是因为 &String 可以被 强转 (coerced )成 &str。当 add 函数被调用时,Rust 使用了一个被称为 Deref 强制转换 (deref coercion )的技术,你可以将其理解为它把 &s2 变成了 &s2[..]。
1 2 3 4 5 6 7 8 9 10 let s1 = String ::from ("tic" ); let s2 = String ::from ("tac" ); let s3 = String ::from ("toe" ); let s = s1 + "-" + &s2 + "-" + &s3; let s1 = String ::from ("tic" ); let s2 = String ::from ("tac" ); let s3 = String ::from ("toe" ); let s = format! ("{}-{}-{}" , s1, s2, s3);
宏 format! 生成的代码使用引用所以不会获取任何参数的所有权
s1, s2, s3 都是借用。
Rust 会 把内容复制到新字符串 (堆上分配):
原来的字符串不受影响。
这里的复制是必需的,因为生成了新的独立字符串。
按索引的形式进行访问String Rust 的字符串不支持索引语法 访问
内部表现
String 是一个 Vec 的封装。
let hello = String::from(“Hola”);
在这里,len 的值是 4 ,这意味着储存字符串 “Hola” 的 Vec 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。
(注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3 。)
1 let hello = String ::from ("Здравствуйте" );
当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。
因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值
字节字符串 字节字符串或者说字节数组
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 32 33 34 35 36 37 38 use std::str ;fn main () { let bytestring : &[u8 ; 21 ] = b"this is a byte string" ; println! ("A byte string: {:?}" , bytestring); let escaped = b"\x52\x75\x73\x74 as bytes" ; println! ("Some escaped bytes: {:?}" , escaped); let raw_bytestring = br"\u{211D} is not escaped here" ; println! ("{:?}" , raw_bytestring); if let Ok (my_str) = str ::from_utf8 (raw_bytestring) { println! ("And the same as text: '{}'" , my_str); } let _quotes = br#"You can also use "fancier" formatting, \ like with normal raw strings"# ; let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82\xbb" ; match str ::from_utf8 (shift_jis) { Ok (my_str) => println! ("Conversion successful: '{}'" , my_str), Err (e) => println! ("Conversion failed: {:?}" , e), }; }
字节,标量值,字型簇 Rust 有三种看待字符串的方式:
1 2 3 4 5 6 fn main (){ let w = "नमस्ते" ; for b in w.bytes (){ println! ("{}" ,b); } }
输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 224 164 168 224 164 174 224 164 184 224 165 141 224 164 164 224 165 135
1 2 3 4 5 6 fn main (){ let w = "नमस्ते" ; for b in w.chars (){ println! ("{}" ,b); } }
输出
字形簇 Grapheme Clusters(最接近所谓的字母)
获取比较复杂,标准库中已经不提供了。 需要借助第三方库。
Rust 不允许对 String 进行索引的一个原因:
索引操作应该小号一个常量时间 O(1)
而 String 无法保证:需要遍历所有的内容,来确定有多少个合法的字符
切割 String 字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串 slice。因此,如果你真的希望使用索引创建字符串 slice 时,Rust 会要求你更明确一些。为了更明确索引并表明你需要一个字符串 slice,相比使用 [] 和单个值的索引,可以使用 [] 和一个 range 来创建含特定字节的字符串 slice:
1 2 3 let hello = "Здравствуйте" ;let s = &hello[0 ..4 ];
s 会是一个 &str,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 s 将会是 “Зд”。
如果获取 &hello[0..1] 会发生什么呢?答案是:Rust 在运行时会 panic
因此切割时不能跨越字符串边界
遍历字符串 无法通过索引的方式去访问字符串中的某个字符,但是可以使用切片的方式 &s1[start..end] ,但是 start 和 end 必须准确落在字符的边界处.
对于标量值: chars()方法
对于字节: bytes()方法、
对于字形簇:很复杂,标准库未提供,中英文都不需要关注字符簇,利用 chars 就可以遍历中英文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fn main () { let s1 = String ::from ("hi,中国" ); let h = &s1[0 ..1 ]; assert_eq! (h, "h" ); let h1 = &s1[3 ..6 ]; assert_eq! (h1, "中" ); } fn main () { for c in "你好,世界" .chars () { println! ("{}" , c) } }
HashMap HashMap 默认使用 SipHash 1-3 哈希算法,该算法对于抵抗 HashDos 攻击非常有效。在性能方面,如果你的 key 是中型大小的,那该算法非常不错,但是如果是小型的 key( 例如整数 )亦或是大型的 key ( 例如字符串 ),那你需要采用社区提供的其它算法来提高性能。
哈希表的算法是基于 Google 的 SwissTable ,你可以在这里 找到 C++ 的实现。
创建 HashMap 创建空 HashMap:new()函数
1 2 3 4 5 6 7 use std::collections::HashMap; let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ), 10 ); scores.insert (String ::from ("Yellow" ), 50 );
HashMap 用的较少,不在 Prelude 中
标准库对其支持较少,没有内置的宏来创建 HashMap
数据存在 heap 中
同构的,即 K 必须为一种类型,V 为另一种类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 use std::collections::HashMap;fn main () { let teams = [ ("Chinese Team" , 100 ), ("American Team" , 10 ), ("France Team" , 50 ), ]; let mut teams_map1 = HashMap::new (); for team in &teams { teams_map1.insert (team.0 , team.1 ); } let teams_map2 : HashMap<_,_> = teams.into_iter ().collect (); assert_eq! (teams_map1, teams_map2); println! ("Success!" ) }
collect 方法创建 HashMap
collect 方法可以将数据收集进一系列的集合类型
1 2 3 4 5 6 7 use std::collections::HashMap; let teams = vec! [String ::from ("Blue" ), String ::from ("Yellow" )]; let initial_scores = vec! [10 , 50 ]; let mut scores : HashMap<_, _> = teams.into_iter ().zip (initial_scores.into_iter ()).collect ();
如果队伍的名字和初始分数分别在两个 vector 中,可以使用 zip 方法来创建一个元组的迭代器 ,其中 “Blue” 与 10 是一对,依此类推。接着就可以使用 collect 方法将这个元组的迭代器转换成一个 HashMap
HashMap 和所有权
对于实现了 Copy trait 的类型(如 i32),值会被复制到 HashMap 中
对于拥有所有权的值(例如 String),值会被移动,所有权会转移给 HashMap
如果把引用插入到 HashMap,值本身不会移动但是在 HashMap 有效的期间,被引用的值必须保持有效
访问 HashMap 中的值 get 方法
1 2 3 4 5 6 7 8 9 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);
for 循环遍历 HashMap
1 2 3 4 5 6 7 8 9 10 use std::collections::HashMap; let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ), 10 ); scores.insert (String ::from ("Yellow" ), 50 ); for (key, value) in &scores { println! ("{}: {}" , key, value); }
这会以任意顺序 打印出每一个键值对:
索引与 get 方法
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 use std::collections::HashMap;fn main () { let mut scores = HashMap::new (); scores.insert ("Sunface" , 98 ); scores.insert ("Daniel" , 95 ); scores.insert ("Ashley" , 69 ); scores.insert ("Katie" , 58 ); let score = scores.get ("Sunface" ); assert_eq! (score, Some (&98 )); if scores.contains_key ("Daniel" ) { let score = scores["Daniel" ]; assert_eq! (score, 95 ); scores.remove ("Daniel" ); } assert_eq! (scores.len (), 3 ); for (name, score) in scores { println! ("The score of {} is {}" , name, score) } }
更新 HashMap 覆盖一个值 1 2 3 4 5 6 7 8 use std::collections::HashMap; let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ), 10 ); scores.insert (String ::from ("Blue" ), 25 ); println! ("{:?}" , scores);
这会打印出 {“Blue”: 25}。原始的值 10 则被覆盖了
只在键没有对应值时插入 使用 entry 方法只在键没有对应一个值时插入
1 2 3 4 5 6 7 8 9 10 11 use std::collections::HashMap; 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 }
entry 方法
检查指定的 K 是否对应一个 V
参数为 K,返回 enum Entry:代表值是否存在
Entry 的or_insert()方法 :
返回:
如果 K 存在,返回到对应的 V 的一个可变引用
如果 K 不存在,将方法参数作为 K 的新值插进去,返回到这个值的可变引用
根据旧值更新一个值 1 2 3 4 5 6 7 8 9 10 11 12 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 ; } println! ("{:?}" , map);
这里 or_insert 返回的是一个可变引用,指向刚 entry 方法插入的键对应的值
哈希函数 HashMap 默认使用一种叫做 SipHash 的哈希函数,它可以抵御涉及哈希表(hash table)1 的拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher trait 的类型。我们并不需要从头开始实现你自己的 hasher;crates.io 上有许多常用哈希算法的 hasher 的库。
HashMap key 的限制 任何实现了 Eq 和 Hash 特征的类型都可以用于 HashMap 的 key,包括:
bool (虽然很少用到,因为它只能表达两种 key)
int, uint 以及它们的变体,例如 u8、i32 等
String 和 &str (提示: HashMap 的 key 是 String 类型时,你其实可以使用 &str 配合 get 方法进行查询
需要注意的是,f32 和 f64 并没有实现 Hash,原因是 浮点数精度 的问题会导致它们无法进行相等比较。
如果一个集合类型的所有字段都实现了 Eq 和 Hash,那该集合类型会自动实现 Eq 和 Hash。例如 Vec 要实现 Hash,那么首先需要 T 实现 Hash。
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::collections::HashMap;#[derive(Hash, Eq, PartialEq, Debug)] struct Viking { name: String , country: String , } impl Viking { fn new (name: &str , country: &str ) -> Viking { Viking { name: name.to_string (), country: country.to_string (), } } } fn main () { let vikings = HashMap::from ([ (Viking::new ("Einar" , "Norway" ), 25 ), (Viking::new ("Olaf" , "Denmark" ), 24 ), (Viking::new ("Harald" , "Iceland" ), 12 ), ]); for (viking, health) in &vikings { println! ("{:?} has {} hp" , viking, health); } }
容量 关于容量,我们在之前的 Vector 中有详细的介绍,而 HashMap 也可以调整容量: 你可以通过 HashMap::with_capacity(uint) 使用指定的容量来初始化,或者使用 HashMap::new() ,后者会提供一个默认的初始化容量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use std::collections::HashMap;fn main () { let mut map : HashMap<i32 , i32 > = HashMap::with_capacity (100 ); map.insert (1 , 2 ); map.insert (3 , 4 ); assert! (map.capacity () >= 100 ); map.shrink_to (50 ); assert! (map.capacity () >= 50 ); map.shrink_to_fit (); assert! (map.capacity () >= 2 ); println! ("Success!" ) }
所有权 对于实现了 Copy 特征的类型,例如 i32,那类型的值会被拷贝到 HashMap 中。而对于有所有权的类型,例如 String,它们的值的所有权将被转移到 HashMap 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use std::collections::HashMap;fn main () { let v1 = 10 ; let mut m1 = HashMap::new (); m1.insert (v1, v1); println! ("v1 is still usable after inserting to hashmap : {}" , v1); let v2 = "hello" .to_string (); let mut m2 = HashMap::new (); m2.insert (v2, v1); assert_eq! (v2, "hello" ); println! ("Success!" ) }
第三方 Hash 库 在开头,我们提到过如果现有的 SipHash 1-3 的性能无法满足需求,那么可以使用社区提供的替代算法。
例如其中一个社区库的使用方式如下:
1 2 3 4 5 6 7 8 9 use std::hash::BuildHasherDefault;use std::collections::HashMap;use twox_hash::XxHash64;let mut hash : HashMap<_, _, BuildHasherDefault<XxHash64>> = Default ::default ();hash.insert (42 , "the answer" ); assert_eq! (hash.get (&42 ), Some (&"the answer" ));
错误处理 大部分情况下,在编译时提示错误,并处理
错误的分类
例如文件未找到,可再次尝试
bug,例如访问索引超出范围
Rust 没有类似 C++/Java 的异常机制
可恢复的错误:Result
不可恢复:panic!宏
不可恢复的错误与 panic! 当 panic!宏执行
程序打印一个错误信息
展开(unwind),清理调用栈(Stack)
退出程序
panic = ‘abort’ 默认情况下,当 panic 发生
Rust 沿着调用栈往回走,清理每个遇到的函数中的数据
不进行清理,直接停止程序,内存需要由 OS 进行清理
想让二进制文件更小,把设置从“展开”改为“中止”
1 2 [profile.release] panic = 'abort'
自己写的代码中内 panic
1 2 3 fn main (){ panic! ("crash and burn" ); }
所以依赖的代码中:外部 panic
1 2 3 4 fn main (){ let vector =vec! [1 ,2 ,3 ]; vector[100 ]; }
输出
1 2 3 4 5 6 Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo) Finished dev [unoptimized + debuginfo] target(s) in 0.18s Running `target\debug\demo.exe` thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100' , src\main.rs:3:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace error: process didn't exit successfully: `target\debug\demo.exe` (exit code: 101)
通过调用 panic!的函数的回溯信息来定位引起问题的代码 使用以下命令运行代码(Windows cmd)
1 set RUST_BACKTRACE=1 && cargo run
Windows Poweshell
1 $Env:RUST_BACKTRACE =1 -and (cargo run)
linux 下
1 export RUST_BACKTRACE=1 && cargo run
更详细的信息
RUST_BACKTRACE=full
为了获取带有调试信息的回溯,必须启用调试符号(不带—release)
Result 枚举与可恢复的错误 1 2 3 4 enum Result <T, E> { Ok (T), Err (E), }
T 代表成功时返回的 Ok 成员中的数据的类型,
而 E 代表失败时返回的 Err 成员中的错误的类型
Result 及其变体也是由 prelude 带入作用域的
打开文件
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! ("Problem opening the file: {:?}" , error), }; }
匹配不同的错误 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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! ("Problem creating the file: {:?}" , e), }, other_error => { panic! ("Problem opening the file: {:?}" , other_error) } }, }; }
使用 unrap_or_else() 和 闭包(closure)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 use std::fs::File;use std::io::ErrorKind;fn main () { let f = File::open ("hello.txt" ).unwrap_or_else (|error| { if error.kind () == ErrorKind::NotFound { File::create ("hello.txt" ).unwrap_or_else (|error| { panic! ("Problem creating the file: {:?}" , error); }) } else { panic! ("Problem opening the file: {:?}" , error); } }); }
unwrap match 表达式的一个快捷方法
1 2 3 4 5 use std::fs::File;fn main () { let f = File::open ("hello.txt" ).unwrap (); }
相当于
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! ("Problem opening the file: {:?}" , error), }; }
如果 Result 结果是 Ok,返回 Ok 里面的值
如果 Result 结果是 Err,调用 panic!宏
expect 可自定义错误信息的 unwrap
1 2 3 4 5 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 17 18 19 20 use std::fs::File;use std::io::{self , Read};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 use std::fs::File;use std::io;use std::io::Read;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) }
? 被定义为与自定义传播错误的示例中定义的处理 Result 值的 match 表达式有着完全相同的工作方式。
如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。
如果值是 Err,Err 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样 ,这样错误值就被传播给了调用者。
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 use std::fs::File;use std::io::{self , Read};fn read_file1 () -> 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), } } fn read_file2 () -> Result <String , io::Error> { let mut s = String ::new (); File::open ("hello.txt" )?.read_to_string (&mut s)?; Ok (s) } fn main () { assert_eq! (read_file1 ().unwrap_err ().to_string (), read_file2 ().unwrap_err ().to_string ()); println! ("Success!" ) }
?与 from 函数 Trait std::convert::From 上的 from 函数
用于错误之间的转换
被?所应用的错误,会隐式地被 from 函数处理
当? 调用 from 函数时:
它所接收地错误类型会被转化为当前函数返回类型所定义的错误类型
用于: 针对不同的错误原因,返回同一种错误类型
只要每个错误类型实现了转换为所返回的错误类型的 from 函数
可以在 ? 之后直接使用链式方法调用 来进一步缩短代码
文件名: src/main.rs
1 2 3 4 5 6 7 8 9 10 11 use std::fs::File;use std::io;use std::io::Read;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) }
自定义实现
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 32 33 34 use std::fmt;use std::fs::File;use std::io::{self , Read};#[derive(Debug)] enum MyError { Io (io::Error), Parse, } impl fmt ::Display for MyError { fn fmt (&self , f: &mut fmt::Formatter<'_ >) -> fmt::Result { match self { MyError::Io (e) => write! (f, "IO error: {}" , e), MyError::Parse => write! (f, "Parse error" ), } } } impl From <io::Error> for MyError { fn from (error: io::Error) -> MyError { MyError::Io (error) } } fn read_username_from_file () -> Result <String , MyError> { let mut s = String ::new (); File::open ("hello.txt" )?.read_to_string (&mut s)?; Ok (s) }
?与 main 函数 main 函数的返回类型为()类型
1 2 3 4 5 6 7 use std::error::Error;use std::fs::File;fn main () -> Result <(), Box <dyn Error>> { let f = File::open ("hello.txt" )?; Ok (()) }
map,and_then map 示例 —— 修改 Ok 里的值
只处理 Ok 的值并返回新的 Result,不会改变错误类型
1 2 3 let x : Result <i32 , &str > = Ok (2 );let y = x.map (|n| n * 3 );assert_eq! (y, Ok (6 ));
如果是错误呢?
1 2 3 let x : Result <i32 , &str > = Err ("error" );let y = x.map (|n| n * 3 );assert_eq! (y, Err ("error" ));
and_then 示例 —— 链式 Result 操作
and_then处理 Ok,并继续返回一个 Result(链式逻辑), 适合把多个可能出错的步骤串起来:
1 2 3 4 5 6 7 8 9 10 fn sq_then_to_string (x: i32 ) -> Result <String , &'static str > { if x < 0 { Err ("negative" ) } else { Ok ((x * x).to_string ()) } } let result = Ok (3 ).and_then (sq_then_to_string);assert_eq! (result, Ok ("9" .to_string ()));
如果出错,自动停止链式计算:
1 2 let result = Err ("bad" ).and_then (sq_then_to_string);assert_eq! (result, Err ("bad" ));
例子:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 use std::num::ParseIntError;fn multiply (n1_str: &str , n2_str: &str ) -> Result <i32 , ParseIntError> { match n1_str.parse::<i32 >() { Ok (n1) => { match n2_str.parse::<i32 >() { Ok (n2) => { Ok (n1 * n2) }, Err (e) => Err (e), } }, Err (e) => Err (e), } } fn multiply1 (n1_str: &str , n2_str: &str ) -> Result <i32 , ParseIntError> { n1_str.parse::<i32 >().and_then (|n1| { n2_str.parse::<i32 >().map (|n2| n1 * n2) }) } fn print (result: Result <i32 , ParseIntError>) { match result { Ok (n) => println! ("n is {}" , n), Err (e) => println! ("Error: {}" , e), } } fn main () { let twenty = multiply1 ("10" , "2" ); print (twenty); let tt = multiply ("t" , "2" ); print (tt); println! ("Success!" ) }
何时 panic! 总体原则
在定义一个可能失败的函数时,优先考虑返回 Result
否则就 panic!
场景
演示某些概念:unwrap
原型代码:unwrap,expect
测试:unwrap,expect
有时你比编译器掌握更多的信息
你可以确定 Result 就是 Ok:unwrap
1 2 use std::net::IpAddr; let home : IpAddr = "127.0.0.1" .parse ().unwrap ();
调用你的代码,传入无意义的参数值:panic!
创建自定义类型进行有效性验证 一种实现方式是将猜测解析成 i32 而不仅仅是 u32,来默许输入负数,接着检查数字是否在范围内:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 loop { let guess : i32 = match guess.trim ().parse () { Ok (num) => num, Err (_) => continue , }; if guess < 1 || guess > 100 { println! ("The secret number will be between 1 and 100." ); continue ; } match guess.cmp (&secret_number) { }
if 表达式检查了值是否超出范围,告诉用户出了什么问题,并调用 continue 开始下一次循环,请求另一个猜测。if 表达式之后,就可以在知道 guess 在 1 到 100 之间的情况下与秘密数字作比较了。
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 中展示了一个定义 Guess 类型的方法,只有在 new 函数接收到 1 到 100 之间的值时才会创建 Guess 的实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pub struct Guess { value: i32 , } impl Guess { pub fn new (value: i32 ) -> Guess { if value < 1 || value > 100 { panic! ("Guess value must be between 1 and 100, got {}." , value); } Guess { value } } pub fn value (&self ) -> i32 { self .value } }
我们实现了一个借用了 self 的方法 value,它没有任何其他参数并返回一个 i32。这类方法有时被称为 getter ,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为 Guess 结构体的 value 字段是私有的。私有的字段 value 是很重要的,这样使用 Guess 结构体的代码将不允许直接设置 value 的值:调用者 必须 使用 Guess::new 方法来创建一个 Guess 的实例,这就确保了不会存在一个 value 没有通过 Guess::new 函数的条件检查的 Guess。
于是,一个接收(或返回) 1 到 100 之间数字的函数就可以声明为接收(或返回) Guess 的实例,而不是 i32,同时其函数体中也无需进行任何额外的检查。
在 fn main 中使用 Result 一个典型的 main 函数长这样:
1 2 3 fn main () { println! ("Hello World!" ); }
事实上 main 函数还可以返回一个 Result 类型:如果 main 函数内部发生了错误,那该错误会被返回并且打印出一条错误的 debug 信息。
1 2 3 4 5 6 7 8 9 10 11 use std::num::ParseIntError;fn main () -> Result <(), ParseIntError> { let number_str = "10" ; let number = match number_str.parse::<i32 >() { Ok (number) => number, Err (e) => return Err (e), }; println! ("{}" , number); Ok (()) }
泛型 函数中定义泛型 寻找 vec 中的最大值
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 fn largest_i32 (list: &[i32 ]) -> i32 { let mut largest = list[0 ]; for &item in list { if item > largest { largest = item; } } largest } fn largest_char (list: &[char ]) -> char { let mut largest = list[0 ]; for &item in list { if item > largest { largest = item; } } largest } fn main () { let number_list = vec! [34 , 50 , 25 , 100 , 65 ]; let result = largest_i32 (&number_list); println! ("The largest number is {}" , result); let char_list = vec! ['y' , 'm' , 'a' , 'q' ]; let result = largest_char (&char_list); println! ("The largest char is {}" , result); }
使用泛型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn largest <T>(list: &[T]) -> T { let mut largest = list[0 ]; for &item in list { if item > largest { largest = item; } } largest } fn main () { let number_list = vec! [34 , 50 , 25 , 100 , 65 ]; let result = largest (&number_list); println! ("The largest number is {}" , result); let char_list = vec! ['y' , 'm' , 'a' , 'q' ]; let result = largest (&char_list); println! ("The largest char is {}" , result); }
运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo) error[E0369]: binary operation `>` cannot be applied to type `T` --> src\main.rs:5:17 | 5 | if item > largest { | ---- ^ ------- T | | | T | help: consider restricting type parameter `T` | 1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T { | ++++++++++++++++++++++ For more information about this error, try `rustc --explain E0369`. error: could not compile `demo` due to previous error
简单来说,这个错误表明 largest 的函数体不能适用于 T 的所有可能的类型
因为在函数体需要比较 T 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 std::cmp::PartialOrd trait 可以实现类型的比较功能.
修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 fn largest <T>(list: &[T]) -> &Twhere T: PartialOrd , { let mut largest = &list[0 ]; for item in list { if *item > *largest { largest = item; } } largest } fn main () { let number_list = vec! [34 , 50 , 25 , 100 , 65 ]; let result = largest (&number_list); println! ("The largest number is {}" , result); let char_list = vec! ['y' , 'm' , 'a' , 'q' ]; let result = largest (&char_list); println! ("The largest char is {}" , result); }
结构体中定义泛型 文件名: src/main.rs
1 2 3 4 5 6 7 8 9 struct Point <T> { x: T, y: T, } fn main () { let integer = Point { x: 5 , y: 10 }; let float = Point { x: 1.0 , y: 4.0 }; }
枚举中定义泛型 1 2 3 4 enum Option <T> { Some (T), None , }
枚举也可以拥有多个泛型类型。
1 2 3 4 enum Result <T, E> { Ok (T), Err (E), }
方法定义中的泛型 文件名: src/main.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct Point <T> { x: T, y: T, } impl <T> Point<T> { fn x (&self ) -> &T { &self .x } } impl Point <i32 > { fn x (&self ) -> &i32 { &self .x } } fn main () { let p = Point { x: 5 , y: 10 }; println! ("p.x = {}" , p.x ()); }
把 T 放在 impl 关键字后,表示在类型 T 上实现方法:impl Point
只针对具体类型实现方法:impl Point
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct Point <T> { x: T, y: T, } impl Point <f32 > { fn distance_from_origin (&self ) -> f32 { (self .x.powi (2 ) + self .y.powi (2 )).sqrt () } } fn main () { let p = Point{x: 5.0_f32 , y: 10.0_f32 }; println! ("{}" ,p.distance_from_origin ()) }
struct 里的泛型类型参数可以和方法的泛型类型参数不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct Point <X1, Y1> { x: X1, y: Y1, } impl <X1, Y1> Point<X1, Y1> { fn mixup <X2, Y2>(self , other: Point<X2, Y2>) -> Point<X1, Y2> { Point { x: self .x, y: other.y, } } } fn main () { let p1 = Point { x: 5 , y: 10.4 }; let p2 = Point { x: "Hello" , y: 'c' }; let p3 = p1.mixup (p2); println! ("p3.x = {}, p3.y = {}" , p3.x, p3.y); }
const 泛型 针对类型实现的泛型,所有的泛型都是为了抽象不同的类型,那有没有针对值的泛型?答案就是 Const 泛型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct ArrayPair <T, const N: usize > { left: [T; N], right: [T; N], } impl <T: Debug , const N: usize > Debug for ArrayPair <T, N> { } fn foo <const N: usize >() {}fn bar <T, const M: usize >() { foo::<M>(); foo::<2021 >(); foo::<{20 * 100 + 20 * 10 + 1 }>(); foo::<{ M + 1 }>(); foo::<{ std::mem::size_of::<T>() }>(); let _ : [u8 ; M]; let _ : [u8 ; std::mem::size_of::<T>()]; } fn main () {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pub struct MinSlice <T, const N: usize > { pub head: [T; N], pub tail: [T], } fn main () { let slice : &[u8 ] = b"Hello, world" ; let reference : Option <&u8 > = slice.get (6 ); assert! (reference.is_some ()); let slice : &[u8 ] = b"Hello, world" ; let minslice = MinSlice::<u8 , 12 >::from_slice (slice).unwrap (); let value : u8 = minslice.head[6 ]; assert_eq! (value, b' ' ) }
`
是结构体类型的一部分,和数组类型一样,这意味着长度不同会导致类型不同: Array 和 Array 是不同的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #[allow(unused)] struct Array <T, const N: usize > { data : [T; N] } fn main () { let arrays = [ Array{ data: [1 , 2 , 3 ], }, Array { data: [1 , 2 , 3 ], }, Array { data: [4 ,5 ,6 ] } ]; }
填空
1 2 3 4 5 6 7 8 9 10 11 fn print_array <__>(__) { println! ("{:?}" , arr); } fn main () { let arr = [1 , 2 , 3 ]; print_array (arr); let arr = ["hello" , "world" ]; print_array (arr); }
答案:
1 2 3 4 5 6 7 8 9 10 fn print_array <T: std::fmt::Debug , const N: usize >(arr: [T; N]) { println! ("{:?}" , arr); } fn main () { let arr = [1 , 2 , 3 ]; print_array (arr); let arr = ["hello" , "world" ]; print_array (arr); }
有时我们希望能限制一个变量占用内存的大小 ,例如在嵌入式环境中,此时 const 泛型参数的第三种形式 const 表达式 就非常适合:
下面的代码用到了 feature,需要 nightly 编译器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #![allow(incomplete_features)] #![feature(generic_const_exprs)] fn check_size <T>(val: T)where Assert<{ core::mem::size_of::<T>() < 768 }>: IsTrue, { } fn main () { check_size ([0u8 ; 767 ]); check_size ([0i32 ; 191 ]); check_size (["hello你好" ; 47 ]); check_size ([(); 31 ].map (|_| "hello你好" .to_string ())); check_size (['中' ; 191 ]); } pub enum Assert <const CHECK: bool > {}pub trait IsTrue {}impl IsTrue for Assert <true > {}
泛型代码的性能 Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。
Rust 通过在编译时进行泛型代码的 单态化 (monomorphization )来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 enum Option_i32 { Some (i32 ), None , } enum Option_f64 { Some (f64 ), None , } fn main () { let integer = Option_i32::Some (5 ); let float = Option_f64::Some (5.0 ); }
在编译时,rust 会将 Option 泛型展开为 Option和 Option类型
Trait:定义共同行为 trait 告诉 Rust 编译器,某种类型具有哪些并且可以与其他类型共享的功能
定义一个 trait 把方法签名放在一起,来定义实现某种目的所必需的一组行为
文件名: src/lib.rs
1 2 3 pub trait Summary { fn summarize (&self ) -> String ; }
在类型上实现 trait 与为类型实现方法类似
不同之处:impl Xxxx for Tweet{….}
文件: src/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 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) } }
文件 src/main.rs
1 2 3 4 5 6 7 8 9 10 use demo::{Summary,Tweet};fn main (){ let tweet = Tweet{ username: String ::from ("horse_ebook" ), content: String ::from ("of course,sa you probably..." ), reply:false , retweet:false , }; println! ("1 new tweet:{}" ,tweet.summary ()); }
demo 就是 Cargo.toml 文件中的[package]项的 name
实现 trait 的约束 可以在某个类型上实现某个 trait 的前提条件是:
整个类型 或 这个 trait 是在本地 crate 里定义的
无法为外部类型实现外部 trait
这个限制是被称为 相干性 (coherence ) 的程序属性的一部分,或者更具体的说是 孤儿规则 (orphan rule ),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。
简而言之:类型和 trait 都不是你写的 ,那你就没有资格给它们加 impl 。
trait 中的函数的默认实现 文件名: src/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 26 27 28 29 30 31 pub trait Summary { fn summarize (&self ) -> String { String ::from ("(Read more...)" ) } } pub struct NewsArticle { pub headline: String , pub location: String , pub author: String , pub content: String , } impl Summary for NewsArticle { } 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) } }
默认实现允许调用相同 trait 中的其他方法 ,哪怕这些方法没有默认实现
1 2 3 4 5 6 7 pub trait Summary { fn summarize_author (&self ) -> String ; fn summarize (&self ) -> String { format! ("(Read more from {}...)" , self .summarize_author ()) } }
注意:无法从方法的重写实现里面调用默认的实现
trait 作为参数 impl triat 语法: 适用于简单情况,是 trait bound 的语法糖
1 2 3 pub fn notify (item: &impl Summary ) { println! ("Breaking news! {}" , item.summarize ()); }
trait bound 语法: 适用于复杂情况
1 2 3 pub fn notify <T: Summary>(item: &T) { println! ("Breaking news! {}" , item.summarize ()); }
比较
1 pub fn notify (item1: &impl Summary , item2: &impl Summary ) {}
这适用于 item1 和 item2 允许是不同类型的情况(只要它们都实现了 Summary)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound 时才有可能:
1 pub fn notify <T: Summary>(item1: &T, item2: &T) {}
使用+指定多个 traint bound 1 2 3 pub fn notify (item: &(impl Summary + Display)) {}pub fn notify <T: Summary + Display>(item: &T) {}
使用 where 简化 trait bound fn some_function(t: &T, u: &U) -> i32 {
使用 where 从句
1 2 3 4 fn some_function <T, U>(t: &T, u: &U) -> i32 where T: Display + Clone , U: Clone + Debug {}
返回实现了 trait 的类型 也可以在返回值中使用 impl Trait 语法,来返回实现了某个 trait 的类型:
1 2 3 4 5 6 7 8 9 10 fn returns_summarizable () -> impl Summary { Tweet { username: String ::from ("horse_ebooks" ), content: String ::from ( "of course, as you probably already know, people" , ), reply: false , retweet: false , } }
不过这只适用于返回单一类型的情况 。例如,这段代码的返回值类型指定为返回 impl Summary,但是返回了 NewsArticle 或 Tweet 就行不通:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 fn returns_summarizable (switch: bool ) -> impl Summary { if switch { NewsArticle { headline: String ::from ( "Penguins win the Stanley Cup Championship!" , ), location: String ::from ("Pittsburgh, PA, USA" ), author: String ::from ("Iceburgh" ), content: String ::from ( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL." , ), } } else { Tweet { username: String ::from ("horse_ebooks" ), content: String ::from ( "of course, as you probably already know, people" , ), reply: false , retweet: false , } } }
这里尝试返回 NewsArticle 或 Tweet。这不能编译,因为 impl Trait 工作方式的限制。
可以使用 dyn 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 30 31 32 33 34 struct Sheep {}struct Cow {}trait Animal { fn noise (&self ) -> String ; } impl Animal for Sheep { fn noise (&self ) -> String { "baaaaah!" .to_string () } } impl Animal for Cow { fn noise (&self ) -> String { "moooooo!" .to_string () } } fn random_animal (random_number: f64 ) -> Box <dyn Animal> { if random_number < 0.5 { Box ::new (Sheep {}) } else { Box ::new (Cow {}) } } fn main () { let random_number = 0.234 ; let animal = random_animal (random_number); println! ("You've randomly chosen an animal, and it says {}" , animal.noise ()); }
特征对象,在数组中使用特征对象 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 32 33 34 35 36 37 38 39 40 trait Bird { fn quack (&self ); } struct Duck ;impl Duck { fn fly (&self ) { println! ("Look, the duck is flying" ) } } struct Swan ;impl Swan { fn fly (&self ) { println! ("Look, the duck.. oh sorry, the swan is flying" ) } } impl Bird for Duck { fn quack (&self ) { println! ("{}" , "duck duck" ); } } impl Bird for Swan { fn quack (&self ) { println! ("{}" , "swan swan" ); } } fn main () { let birds :[Box <dyn Bird>;2 ]=[Box ::new (Duck{}),Box ::new (Swan{})]; for bird in birds { bird.quack (); } }
&dyn 和 Box 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 32 33 34 trait Draw { fn draw (&self ) -> String ; } impl Draw for u8 { fn draw (&self ) -> String { format! ("u8: {}" , *self ) } } impl Draw for f64 { fn draw (&self ) -> String { format! ("f64: {}" , *self ) } } fn main () { let x = 1.1f64 ; let y = 8u8 ; draw_with_box (Box ::new (x)); draw_with_ref (&y); } fn draw_with_box (x: Box <dyn Draw>) { x.draw (); } fn draw_with_ref (x: &dyn Draw) { x.draw (); }
静态分发和动态分发 Static and Dynamic dispatch 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 trait Foo { fn method (&self ) -> String ; } impl Foo for u8 { fn method (&self ) -> String { format! ("u8: {}" , *self ) } } impl Foo for String { fn method (&self ) -> String { format! ("string: {}" , *self ) } } fn static_dispatch <T: Foo>(x: T) { x.method (); } fn dynamic_dispatch (x: &dyn Foo) { x.method (); } fn main () { let x = 5u8 ; let y = "Hello" .to_string (); static_dispatch (x); dynamic_dispatch (&y); println! ("Success!" ) }
使用 trait bounds 来修复 largest 函数 文件名: src/main.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 fn largest <T: PartialOrd + Copy >(list: &[T]) -> T { let mut largest = list[0 ]; for &item in list { if item > largest { largest = item; } } largest } fn main () { let number_list = vec! [34 , 50 , 25 , 100 , 65 ]; let result = largest (&number_list); println! ("The largest number is {}" , result); let char_list = vec! ['y' , 'm' , 'a' , 'q' ]; let result = largest (&char_list); println! ("The largest char is {}" , result); }
如果并不希望限制 largest 函数只能用于实现了 Copy trait 的类型,我们可以在 T 的 trait bounds 中指定 Clone 而不是 Copy。并克隆 slice 的每一个值使得 largest 函数拥有其所有权。使用 clone 函数意味着对于类似 String 这样拥有堆上数据的类型,会潜在的分配更多堆上空间,而堆分配在涉及大量数据时可能会相当缓慢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn largest <T: PartialOrd + Clone >(list: &[T]) -> T { let mut largest = list[0 ].clone (); for item in list { if item > &largest { largest = item.clone (); } } largest } fn main () { let str_list = vec! [String ::from ("hello" ),String ::from ("world" )]; let result = largest (&str_list) println! ("The largest word is {}" , result); }
或者 largest 直接返回一个引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn largest <T: PartialOrd + Clone >(list: &[T]) -> &T { let mut largest = &list[0 ]; for item in list { if item > &largest { largest = item; } } largest } fn main () { let str_list = vec! [String ::from ("hello" ),String ::from ("world" )]; let result = largest (&str_list) println! ("The largest word is {}" , result); }
使用 trait bound 有条件地实现方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 use std::fmt::Display;struct Pair <T> { x: T, y: T, } impl <T> Pair<T> { fn new (x: T, y: T) -> Self { Self { x, y } } } impl <T: Display + PartialOrd > Pair<T> { fn cmp_display (&self ) { if self .x >= self .y { println! ("The largest member is x = {}" , self .x); } else { println! ("The largest member is y = {}" , self .y); } } }
为满足 trait bound 的所有类型上实现 trait 叫做覆盖实现(blanket implementations)
例如:
1 2 3 impl<T: Display> ToString for T { // --snip-- }
因为标准库有了这些 blanket implementation,我们可以对任何实现了 Display trait 的类型调用由 ToString 定义的 to_string 方法。例如,可以将整型转换为对应的 String 值,因为整型实现了 Display:
let s = 3.to_string();
Derive 宏派生实现 我们可以使用 #[derive] 属性来派生一些特征,对于这些特征编译器会自动进行默认实现,对于日常代码开发而言,这是非常方便的,例如大家经常用到的 Debug 特征,就是直接通过派生来获取默认实现,而无需我们手动去完成这个工作。
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 32 33 34 35 36 37 38 39 40 41 42 43 #[derive(PartialEq, PartialOrd)] struct Centimeters (f64 );#[derive(Debug)] struct Inches (i32 );impl Inches { fn to_centimeters (&self ) -> Centimeters { let &Inches (inches) = self ; Centimeters (inches as f64 * 2.54 ) } } #[derive(Debug,PartialEq,PartialOrd)] struct Seconds (i32 );fn main () { let _one_second = Seconds (1 ); println! ("One second looks like: {:?}" , _one_second); let _this_is_true = _one_second == _one_second; let _this_is_true = _one_second > _one_second; let foot = Inches (12 ); println! ("One foot equals {:?}" , foot); let meter = Centimeters (100.0 ); let cmp = if foot.to_centimeters () < meter { "smaller" } else { "bigger" }; println! ("One foot is {} than one meter." , cmp); }
生命周期
Rust 的每个引用都有自己的生命周期
生命周期:引用保持有效的作用域
大多数情况下:生命周期是隐式的,可被推断的
当引用的生命周期可能以不同的方式互相关联时:手动标注生命周期
生命周期存在的目标是:避免悬空引用
尝试使用离开作用域的值的引用会失败
1 2 3 4 5 6 7 8 9 10 { let r ; { let x = 5 ; r = &x; } println! ("r: {}" , r); }
借用检查器 Rust 编译器有一个 借用检查器 (borrow checker ),它比较作用域来确保所有的借用都是有效的。
1 2 3 4 5 6 7 8 9 10 { let r ; { let x = 5 ; r = &x; } println! ("r: {}" , r); }
这里将 r 的生命周期标记为 ‘a 并将 x 的生命周期标记为 ‘b。如你所见,内部的 ‘b 块要比外部的生命周期 ‘a 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 ‘a,不过它引用了一个拥有生命周期 ‘b 的对象。程序被拒绝编译,因为生命周期 ‘b 比生命周期 ‘a 要小:被引用的对象比它的引用者存在的时间更短。
函数中的泛型生命周期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn longest (x: &str , y: &str ) -> &str { if x.len () > y.len () { x } else { y } } fn main () { let string1 = String ::from ("abcd" ); let string2 = "xyz" ; let result = longest (string1.as_str (), string2); println! ("The longest string is {}" , result); }
执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 error[E0106]: missing lifetime specifier --> src\main.rs:1:33 | 1 | fn longest(x: &str, y: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help : this function 's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y` help: consider introducing a named lifetime parameter | 1 | fn longest<' a>(x: &'a str, y: &' a str) -> &'a str { | ++++ ++ ++ ++ For more information about this error, try `rustc --explain E0106`. error: could not compile `demo` due to previous error
标识生命周期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn longest <'a >(x: &'a str , y: &'a str ) -> &'a str { if x.len () > y.len () { x } else { y } } fn main () { let string1 = String ::from ("abcd" ); let string2 = "xyz" ; let result = longest (string1.as_str (), string2); println! ("The longest string is {}" , result); }
表示返回值的生命周期是传入参数的两个引用的生命周期的重叠部分。
生命周期标注语法
在引用的&符号后 ,使用空格将标注和引用类型分开
1 2 3 &i32 &'a i32 &'a mut i32
泛型生命周期参数声明在:函数名和参数列表之间的<>里
fn longest<’a>(x: &’a str, y: &’a str) -> &’a str {
1 2 3 4 5 6 7 fn longest <'a >(x: &'a str , y: &'a str ) -> &'a str { if x.len () > y.len () { x } else { y } }
它的实际含义是 longest 函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。
记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 longest 函数并不需要知道 x 和 y 具体会存在多久,而只需要知道有某个可以被 ‘a 替代的作用域将会满足这个签名。
当具体的引用被传递给 longest 时,被 ‘a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分 。换一种说法就是泛型生命周期 ‘a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个 。因为我们用相同的生命周期参数 ‘a 标注了返回的引用值,所以返回的引用值就能保证在 x 和 y 中较短的那个生命周期结束之前保持有效。
使用
1 2 3 4 5 6 7 8 9 fn main () { let string1 = String ::from ("long string is long" ); { let string2 = String ::from ("xyz" ); let result = longest (string1.as_str (), string2.as_str ()); println! ("The longest string is {}" , result); } }
在这个例子中,string1 直到外部作用域结束都是有效的,string2 则在内部作用域中是有效的,而 result 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 The longest string is long string is long。
修改
1 2 3 4 5 6 7 8 9 fn main () { let string1 = String ::from ("long string is long" ); let result ; { let string2 = String ::from ("xyz" ); result = longest (string1.as_str (), string2.as_str ()); } println! ("The longest string is {}" , result); }
程序运行出错
错误表明为了保证 println! 中的 result 是有效的,string2 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(longest)函数的参数和返回值都使用了相同的生命周期参数 ‘a。
深入理解声明周期
如果将 longest 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y 指定一个生命周期。如下代码将能够编译:
文件名: src/main.rs
1 2 3 fn longest <'a >(x: &'a str , y: &str ) -> &'a str { x }
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。
如果返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。
1 2 3 4 fn longest <'a >(x: &str , y: &str ) -> &'a str { let result = String ::from ("really long string" ); result.as_str () }
编译报错
综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的 。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
结构体定义中的生命周期注解 struct 里可包括:
自持有类型
引用:需要在每个引用上添加声明周期标注
文件名: src/main.rs
1 2 3 4 5 6 7 8 9 10 11 struct ImportantExcerpt <'a > { part: &'a str , } fn main () { let novel = String ::from ("Call me Ishmael. Some years ago..." ); let first_sentence = novel.split ('.' ).next ().expect ("Could not find a '.'" ); let i = ImportantExcerpt { part: first_sentence, }; }
表示结构体的生命周期和其成员 part 的生命周期相同。
生命周期的省略 Lifetime Elision
在 Rust 引用分析中所编入的模式称为生命周期省略规则 (lifetime elision rules )
这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。
函数或方法的参数的生命周期被称为 输入生命周期 (input lifetimes ),而返回值的生命周期被称为 输出生命周期 (output lifetimes )。
编译器采用三条规则 来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn 定义,以及 impl 块 。
规则 1 :每个引用类型的参数都有自己的声明周期
规则 2 :如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
规则 3 :如果有多个输入生命周期参数,但其中一个是&self 或&mut self(是方法),那么 self 的生命周期就会被赋给所有的输出生命周期参数
例子:
假设我们自己就是编译器。
1 fn first_word (s: &str ) -> &str {
接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期 。
1 fn first_word <'a >(s: &'a str ) -> &str {
对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:
1 fn first_word <'a >(s: &'a str ) -> &'a str {
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。
1 fn longest (x:&str ,y:&str )-> &str {
应用第一条规则
1 fn longest <'a ,'b >(x:&'a str ,y:&'b str )-> &str {
第二条规则不适用,第三条不适用,所以编译器报错
方法定义中的生命周期标注 当为带有生命周期的结构体实现方法时,其语法依然类似泛型类型参数的语法。
在哪里声明生命周期参数,依赖于:
生命周期参数是否同结构体字段或方法参数和返回值相关 。
struct 字段的生命周期名:
在 impl 后声明
在 struct 名后使用
这些生命周期是 struct 类型的一部分
impl 块内的方法签名中:
引用必须绑定于 struct 字段引用的声明周期,或者引用是独立的也可以
生命周期省略规则经常使得方法中的生命周期标注不是必须的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct ImportantExcerpt <'a >{ part: &'a str , } impl <'a > ImportantExcerpt<'a > { fn level (&self ) -> i32 { 3 } fn announce_and_return_part (&self , announcement: &str ) -> &str { println! ("Attention please: {}" , announcement); self .part } } fn main (){ let novel = String ::from ("Call me Ishmael.Some year ago..." ); let first_sentence = novel.split ('.' ).next ().expect ("Could not found a '.'" ); let i = ImportantExcerpt{ part: first_sentence, }; }
announce_and_return_part 这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予 &self 和 announcement 他们各自的生命周期。接着,因为其中一个参数是 &self,返回值类型被赋予了 &self 的生命周期,这样所有的生命周期都被计算出来了。
静态生命周期 ‘static,其生命周期能够 存活于整个程序期间。
所有的字符串字面值都拥有 ‘static 生命周期
let s: &’static str = “I have a static lifetime.”;
这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 ‘static 的。
结合泛型类型参数、trait bounds 和生命周期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 use std::fmt::Display;fn longest_with_an_announcement <'a , T>( x: &'a str , y: &'a str , ann: T, ) -> &'a str where T: Display, { println! ("Announcement! {}" , ann); if x.len () > y.len () { x } else { y } }
编写自动化测试 Rust 中的测试函数是用来验证非测试代码是否是按照期望的方式运行的。测试函数体通常执行如下三种操作:
设置任何所需的数据或状态(Arrange)
运行需要测试的代码(Act)
断言(Assert)其结果是我们所期望的
测试函数剖析 编写测试函数 Rust 中的测试就是一个带有 test 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据
为了将一个函数变成测试函数,需要在 fn 行之前加上 #[test]
运行测试
Rust Hui 构建一个 Test Runner 的可执行文件,它会运行标注了 test 的函数,并报告其运行是否成功
当使用 cargo 创建 library 项目时,会生成一个 test module,里面有一个 test 函数
可以添加任意数量的 test module 或函数
src/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pub fn add (left: usize , right: usize ) -> usize { left + right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works () { let result = add (2 , 2 ); assert_eq! (result, 4 ); } }
运行
测试失败
测试函数 panic 就表示失败
每个测试运行在一个新线程
当主线程看见某个测试线程挂掉了,那个测试标记为失败了
断言(Assert) 使用 assert!宏检查测试结果 assert!宏,来自标准库,用来确定某个状态是否为 true
true,测试通过
false, 调用 panic!,测试失败
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 #[derive(Debug)] struct Rectangle { width: u32 , height: u32 , } impl Rectangle { fn can_hold (&self , other: &Rectangle) -> bool { self .width > other.width && self .height > other.height } } #[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller () { let larger = Rectangle { width: 8 , height: 7 , }; let smaller = Rectangle { width: 5 , height: 1 , }; assert! (larger.can_hold (&smaller)); } }
使用 assert_eq! 和 assert_ne! 宏来测试相等
都来自于标准库
判断两个参数是否相等或不等
实际上,它们使用的就是==和!=运算符的 assert!
断言失败,会自动打印出两个参数的值
使用 debug 格式打印参数:要求参数实现了 PartialEq 和 Debug Traits(所有基本类型和标准库大部分类型都实现了)
1 2 3 4 5 6 7 8 9 10 11 12 13 pub fn add_two (a: i32 ) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two () { assert_eq! (4 , add_two (2 )); } }
自定义错误信息 可以向 assert!、assert_eq! 和 assert_ne! 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。
assert! 宏的第一个参数必填,自定义消息作为第二个参数
assert_eq!和 assert_ne! 前两个参数必填,自定义消息作为第 3 个参数
自定义消息参数会被传递给 format!宏,可以使用{}占位符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pub fn greeting (name: &str ) -> String { format! ("Hello {}!" , name) } #[cfg(test)] mod tests { use super::*; #[test] fn greeting_contains_name () { let result = greeting ("Carol" ); assert! (result.contains ("Carol" )); } }
自定义错误信息
1 2 3 4 5 6 7 8 9 #[test] fn greeting_contains_name () { let result = greeting ("Carol" ); assert! ( result.contains ("Carol" ), "Greeting did not contain name, value was `{}`" , result ); }
验证错误处理的情况 可验证代码在特定情况下是否发生了 panic
should_panic 属性(attribute):
函数 panic:测试通过
函数没有 panic:测试失败
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 pub struct Guess { value: i32 , } impl Guess { pub fn new (value: i32 ) -> Guess { if value < 1 || value > 100 { panic! ("Guess value must be between 1 and 100, got {}." , value); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn greater_than_100 () { Guess::new (200 ); } }
让 should_panic 更精确 可以给 should_panic 属性增加一个可选的 expected 参数。测试工具会确保错误信息中包含其提供的文本
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 impl Guess { pub fn new (value: i32 ) -> Guess { if value < 1 { panic! ( "Guess value must be greater than or equal to 1, got {}." , value ); } else if value > 100 { panic! ( "Guess value must be less than or equal to 100, got {}." , value ); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic(expected = "Guess value must be less than or equal to 100" )] fn greater_than_100 () { Guess::new (200 ); } }
在测试中使用 Result 无需 panic,可以使用 Result作为返回类型编写测试
1 2 3 4 5 6 7 8 9 10 11 #[cfg(test)] mod tests { #[test] fn it_works () -> Result <(), String > { if 2 + 2 == 4 { Ok (()) } else { Err (String ::from ("two plus two does not equal four" )) } } }
注意:不能对这些使用 Result 的测试使用 #[should_panic] 注解 。
因为运行失败时会返回 Err 而不会发生 panic
控制测试运行 改变 cargo test 的行为:添加命令行参数
默认行为:
并行运行
所有测试
捕获(不显示)所有标准输出,使读取与测试结果相关的输出更容易
命令行参数:
针对 cargo test 的参数: 紧跟 cargo test 后
cargo test —help
cargo test — —help
并行运行测试 默认使用多个线程并行运行
要确保测试之间:
不会相互依赖
不依赖于某个共享状态(环境,工作目录,环境变量等)
—test-threads 参数 如果你不希望测试并行运行,或者想要更加精确的控制线程的数量,可以传递 —test-threads 参数和希望使用线程的数量给测试二进制文件。例如:
1 cargo test -- --test-threads=1
这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过在有共享的状态时,测试就不会潜在的相互干扰了。
显式函数输出 默认,如果测试通过,Rust 的 test 库会捕获所有打印到标准输出的内容
比如 println!:
如果测试成功,我们将不会在终端看到 println! 的输出 :只会看到说明测试通过的提示行。
如果测试失败了,则会看到所有标准输出和其他错误信息 。
如果你希望也能看到通过的测试中打印的值,也可以在结尾加上 —show-output 告诉 Rust 显示成功测试的输出。
1 cargo test -- --show-output
按名称运行测试的子集 如果没有传递任何参数就运行测试,所有测试都会并行运行:
运行单个测试 可以向 cargo test 传递任意测试的名称来只运行这个测试:
过滤运行多个测试 我们可以指定部分测试的名称,任何名称匹配这个名称的测试会被运行。例如,因为头两个测试的名称包含 add,可以通过 cargo test add 来运行这两个测试:
1 2 3 4 5 6 7 8 9 10 $ cargo test add Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests (target/debug/deps/adder-92948b65e88960b4) running 2 tests test tests::add_three_and_two ... oktest tests::add_two_and_two ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
这运行了所有名字中带有 add 的测试,也过滤掉了名为 one_hundred 的测试。
忽略某些测试 有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 cargo test 的时候希望能排除他们
可以使用 ignore 属性来标记耗时的测试并排除他们,如下所示:
文件名: src/lib.rs
1 2 3 4 5 6 7 8 9 10 #[test] fn it_works () { assert_eq! (2 + 2 , 4 ); } #[test] #[ignore] fn expensive_test () { }
对于想要排除的测试,我们在 #[test] 之后增加了 #[ignore] 行。现在如果运行测试,就会发现 it_works 运行了,而 expensive_test 没有运行:
如果我们只希望运行被忽略的测试,可以使用 cargo test — —ignored
1 $ cargo test -- --ignored
测试组织 Rust 社区倾向于根据测试的两个主要分类来考虑问题:
单元测试 (unit tests )与 集成测试 (integration tests )
元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。
而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。
单元测试 #[cfg(test)]标注 test 模块
只有运行 cargo test 才编译和运行代码
运行 cargo build 则不会
集成测试在不同的目录,它不需要#[cfg(test)]标注
cfg: configuration 配置
告诉 Rust 下面的条目只有在指定的配置选项下才被包含
测试私有函数 测试社区中一直存在关于是否应该对私有函数直接进行测试的论战,而在其他语言中想要测试私有函数是一件困难的,甚至是不可能的事。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测试私有函数 。
文件名: src/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pub fn add_two(a: i32) -> i32 { internal_adder(a, 2) } fn internal_adder(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn internal() { assert_eq!(4, internal_adder(2, 2)); } }
示例 11-12:测试私有函数
注意 internal_adder 函数并没有标记为 pub。同时 tests 也仅仅是另一个模块。正如 “路径用于引用模块树中的项”部分所说,子模块的项可以使用其上级模块的项。在测试中,我们通过 use super::* 将 test 模块的父模块的所有项引入了作用域,接着测试调用了 internal_adder。
集成测试 在 Rust 里,集成测试完全位于测试库的外部
同其他使用库的代码一样使用库文件,也就是说它们只能调用一部分库中的公有 API 。集成测试的目的是测试库的多个部分能否一起正常工作。一些单独能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率 也是很重要的。
tests 目录
创建集成测试:tests 目录
tests 目录下的每个测试文件都是单独的一个 crate
创建一个集成测试。保留示例 adder 中 src/lib.rs 的代码。创建一个 tests 目录,新建一个文件 tests/integration_test.rs ,并输入示例中的代码。
1 2 3 4 5 6 use adder;#[test] fn it_adds_two () { assert_eq! (4 , adder::add_two (2 )); }
并不需要将 tests/integration_test.rs 中的任何代码标注为 #[cfg(test)]。 tests 文件夹在 Cargo 中是一个特殊的文件夹 , Cargo 只会在运行 cargo test 时编译这个目录中的文件。
需要将被测试库导入
运行指定的集成测试 运行一个特定的集成测试
运行某个测试文件内的所有测试
集成测试中的子模块 随着集成测试的增加,你可能希望在 tests 目录增加更多文件以便更好的组织他们,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 tests 目录中的文件都被编译为单独的 crate。
将每个集成测试文件当作其自己的 crate 来对待,这更有助于创建单独的作用域,这种单独的作用域能提供更类似与最终使用者使用 crate 的环境。
例如,如果我们可以创建 一个tests/common.rs 文件并创建一个名叫 setup 的函数,我们希望这个函数能被多个测试文件的测试函数调用:
文件名: tests/common.rs
如果再次运行测试,将会在测试结果中看到一个新的对应 common.rs 文件的测试结果部分,即便这个文件并没有包含任何测试函数,也没有任何地方调用了 setup 函数:
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 $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.89s Running unittests (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::internal ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/common.rs (target/debug/deps/common-92948b65e88960b4) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4) running 1 test test it_adds_two ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
我们并不想要 common 出现在测试结果中显示 running 0 tests 。我们只是希望其能被其他多个集成测试文件中调用罢了。
为了不让 common 出现在测试输出中,我们将创建 tests/common/mod.rs ,而不是创建 tests/common.rs 。这是一种 Rust 的命名规范,这样命名告诉 Rust 不要将 common 看作一个集成测试文件 。将 setup 函数代码移动到 tests/common/mod.rs 并删除 tests/common.rs 文件之后,测试输出中将不会出现这一部分。tests 目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中 。
一旦拥有了 tests/common/mod.rs ,就可以将其作为模块以便在任何集成测试文件中使用。这里是一个 tests/integration_test.rs 中调用 setup 函数的 it_adds_two 测试的例子:
文件名: tests/integration_test.rs
1 2 3 4 5 6 7 8 9 use adder;mod common;#[test] fn it_adds_two () { common::setup (); assert_eq! (4 , adder::add_two (2 )); }
针对 binary crate 的集成测试
IO 项目:构建命令行程序 接收命令行参数 1 2 3 4 5 6 use std::env;fn main () { let args : Vec <String > = env::args ().collect (); println! ("{:?}" , args); }
注意:
std::env::args 在其任何参数包含无效 Unicode 字符时会 panic。
如果你需要接受包含无效 Unicode 字符的参数,使用 std::env::args_os 代替。这个函数返回 OsString 值 而不是 String 值。这里出于简单考虑使用了 std::env::args,因为 OsString 值每个平台都不一样而且比 String 值处理起来更为复杂。
二进制项目的关注与分离 main 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 main 函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤:
将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中 。
当命令行解析逻辑比较小时,可以保留在 main.rs 中。
当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs 中。
经过这些过程之后保留在 main 函数中的责任应该被限制为:
使用参数值调用命令行解析逻辑
设置任何其他的配置
调用 lib.rs 中的 run 函数
如果 run 返回错误,则处理这个错误
这个模式的一切就是为了关注分离:main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。因为不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试他们。仅仅保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性。
TDD 测试驱动开发 遵循测试驱动开发(Test Driven Development, TDD)的模式来逐步增加 minigrep 的搜索逻辑。这是一个软件开发技术,它遵循如下步骤:
编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
编写或修改足够的代码来使新的测试通过。
重构刚刚增加或修改的代码,并确保测试仍然能通过。
从步骤 1 开始重复!
这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。
编写 minigrep 代码 src/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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 use std::env;use std::error::Error;use std::fs;pub fn run (config: Config) -> Result <(), Box <dyn Error>> { let contents = fs::read_to_string (&config.filename)?; let results = if config.case_sensitive { search (&config.query, &contents) } else { search_case_insensitive (&config.query, &contents) }; for line in results { println! ("{}" , line); } Ok (()) } pub struct Config { pub query: String , pub filename: String , pub case_sensitive: bool , } impl Config { pub fn new (args: &[String ]) -> Result <Config, &'static str > { if args.len () < 3 { return Err ("not enough arguments" ); } let query = args[1 ].clone (); let filename = args[2 ].clone (); let case_sensitive = env::var ("CASE_INSENSITIVE" ).is_err (); Ok (Config { query, filename, case_sensitive, }) } } pub fn search <'a >(query: &str , contents: &'a str ) -> Vec <&'a str > { let mut result = Vec ::new (); for line in contents.lines () { if line.contains (query) { result.push (line); } } result } pub fn search_case_insensitive <'a >(query: &str , contents: &'a str ) -> Vec <&'a str > { let mut result = Vec ::new (); let query = query.to_lowercase (); for line in contents.lines () { if line.to_lowercase ().contains (&query) { result.push (line); } } result } #[cfg(test)] mod tests { use super::*; #[test] fn case_sensitive () { let query = "duct" ; let contents = "\ Rust: safe,fast,productive. Pick three." ; assert_eq! (vec! ["safe,fast,productive." ], search (query, contents)); } #[test] fn case_insensitive () { let query = "duct" ; let contents = "\ Rust: safe,fast,productive. Pick three." ; assert_eq! ( vec! ["safe,fast,productive." ], search_case_insensitive (query, contents) ); } }
src/main.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use std::env;use std::process;use minigrep::Config;fn main () { let args : Vec <String > = env::args ().collect (); let config = Config::new (&args).unwrap_or_else (|err| { eprintln! ("Problem parsing arguments:{}" , err); process::exit (1 ); }); if let Err (e) = minigrep::run (config){ eprintln! ("Application error: {}" ,e); process::exit (1 ); }; }