前言
我是先整体看了下rust book 和rust example 之后培养了一些感觉再整体看下programming rust加深理解。2022年正好出了第二版,我是看的第二版的内容。读书笔记只会记录一些自己认为有价值的点或者思考总结,方便后续自己回看知识点。此外第二版也有一些网友翻译了,有需要直接也可以看中文翻译[1][2]
第三章:基础类型
invoke method优先级比符号高,求绝对值得这么写:(-4).abs()
用下划线可以增加可读性:4_429
rust中字符可以表示为u8类型,例如b’A’表示字符’A’的ASCII码,即65,类型是u8;负责的字符字面量不容易表示的可以用十六进制,比如ESC可以是b’\x1b’
注意rust的整数溢出处理 :–release下整数溢出不会panic,整数值会回绕,例如u8的255+1又变成0。开发模式下会panic。这算是一种性能优化。
区别于很多语言支持类型隐式转换,rust在数值类型上不使用隐式转换,可以避免一些bug。例如:1000 as u8或者232 as i8
数组初始化可以用[u8,10] ,如果需要指定初始值可以用[0u8,10],这样就初始化了10个值为0的数组;向量创建用vec!宏类似的创建,如下。注意vector扩容会性能下降,可以考虑使用Vec::with_capacity创建特定大小的vector
1 let v: Vec<i32> = vec![0; 10]; // 创建一个包含 10 个初始值为 0 的元素的 Vec
区别于别的语言,char类型是存放的unicode字节,而不是字符。存储上是UTF8实现的,因此rust中一个char可能是1~4字节。
slice因为没指定长度,不能编译时确定大小,所以采用引用传递,类型表示为[T]。slice是胖指针,包含指向的第一个元素和包含的元素数量。一般指向的都是集合类、string中的部分内容。切片上的操作主要就是遍历和基于index的访问。&str也是个slice类型,底层表示是&[u8]
字符串反斜杠结尾,可以表示删除下一行的换行符和前缀空格。不转移的字符串可以考虑r### (#的数量可以自己定)
tuple、enum、数组定义都允许最后多个单独的逗号,方便写程序
1 2 3 4 5 6 7 8 enum MyEnum { Variant1, Variant2, Variant3, } let my_array = [1 , 2 , 3 ,];
处理非UTF8字符的一些原则:
在处理文件名时,请使用 std::path::PathBuf 和 &Path;
在处理完二进制数据时,请使用 Vec 和 &[u8];
在处理操作系统的环境变量名称和命令行参数时,请使用 OsString 和 &OsStr;
当与使用空终止字符串的 C 库互操作时,请使用 std::ffi::CString 和 &CStr;
byte string不可以使用slice的方法
1 2 3 4 let my_byte_string = b"hello, world" ;let mut my_byte_array = my_byte_string.to_vec ();let my_slice = &mut my_byte_array[1 ..3 ];
string slice相当于是&[u8], len()方法拿到的是字节数,不是字符数。如果想获取字符数可以使用:
1 my_slice.chars ().count ()
理解&str和String区别。一个是string slice,用&[u8]定义,是指向一个字节数组的指针,String可以类比Vec。&Vec自动转成&[T],&String自动转成&str
可以用type关键字申明类型别名
第四章:所有权与移动
所有权
理解move本质是一个浅拷贝动作,然后旧的栈帧地址改成了未初始化。rust使用未初始化的内存则报错。
对象只有一个owner
对比python、java和C++中赋值操作的本质有助于理解rust中赋值的含义。rust move操作相比别的语言主要优点就是默认情况下避免了值共享。
理解Vec的内存模型。栈帧上保留容量长度,值保存在堆上。vec拥有其堆上的对象。
move
基本类型并不会额外指向heap或者再结束以后需要额外做一些动作(Copy类型),没有竞争导致的问题,所以没必要支持move来避免竞争的副作用
集合类注意使用push、take等不会move的方法,直接用赋值操作取值直接move很容易导致集合直接不可用。另外for in 遍历集合的时候注意使用引用 &vec,避免调用into_iter导致move
copy类型
struct全部字段是基础类型时,配合显式加上trait derived copy可以指定其是copy的
共享所有权
Rc、Arc、RefCell等智能指针都是为了达到共享所有权的效果(不能修改数据),这在工程上需要的,因为正常使用对象的时候,一个对象的所有者是唯一的。Arc是线程安全的。Rc是不允许修改的。Rc可以避免循环引用,因为是不可修改的,所以没法构造环。
RC、Arc等共享所有权的智能指针,并没有造成所有权转移,而是通过引用计数来支持共享
补充思考
产生move的场景
将一个值赋给另一个变量时,会发生 move 操作。
将一个值作为函数参数传递时,会发生 move 操作。
通过返回值将一个值从函数中返回时,会发生 move 操作。
将一个值存储到容器中(例如 Vec、HashMap、HashSet 等)时,会发生 move 操作。
不会产生move的场景
使用引用来访问值时,不会发生 move 操作。例如使用 & 来获取一个值的引用。
对于 Copy 类型的值,复制操作会复制整个值,不会发生 move 操作。例如基本数据类型、元组(当元组中所有成员都是 Copy 类型时)等。
在某些特殊情况下,可以使用智能指针(例如 Rc、Arc、Box 等)来共享数据所有权,从而避免 move 操作。
在使用 RefCell 进行内部可变性操作时,可以避免 move 操作,但需要遵循 Rust 的借用规则,避免出现多个可变引用同时存在的情况。
集合类move总结
场景
产生 move
不产生 move
集合操作
针对非 Copy
类型对象调用修改操作(如 push
、insert
、remove
等),会改变集合大小和元素,需要重新分配内存和复制元素。
针对 Copy
类型对象的操作,如 i32
、f64
、char
等。
遍历
使用 into_iter
方法遍历集合时,通常会发生 move 操作,因为它会将集合转移为一个迭代器,然后遍历迭代器并取出元素。
使用 iter
或 iter_mut
方法遍历集合时,不发生 move。
变量传递
将集合类型(如 Vec
、String
、HashMap
等)的所有权从一个变量转移到另一个变量时。
将集合类型作为引用(&
或 &mut
)传递时。
函数参数和返回
将集合类型作为函数参数传递,且函数签名不使用引用时。将集合类型作为函数的返回值。
将集合类型作为函数参数传递,且函数签名使用引用。
所有权是否转移的记忆
判断是否move的主要规则:
变量具有唯一所有权:一个资源(如堆分配的内存)在同一时间只能有一个变量拥有它的所有权。这有助于避免数据竞争和悬垂指针。
所有权转移发生在以下情况:
将一个变量赋值给另一个变量时(如 let s2 = s1;)
将一个变量作为函数参数传递时(如 fn foo(s: String) {})
将一个变量作为函数的返回值时(如 fn bar() -> String { s })
Copy trait:对于实现了 Copy trait 的类型,它们的所有权不会转移,而是会发生复制。例如,基本数值类型(如 i32、f64 等)和布尔类型都实现了 Copy trait。
引用(借用):使用 & 符号可以创建一个变量的引用,而不会发生所有权转移。这允许你在不转移所有权的情况下在多个地方使用同一个资源。引用分为不可变引用(如 &T)和可变引用(如 &mut T)。
针对一个对象调用的函数是否产生move,可以有以下技巧:
考虑对象类型和所有权 :是否涉及 move 取决于对象类型以及你如何操作对象。例如,当操作 Copy trait 类型时,通常不会涉及 move;当操作引用时,通常不会涉及 move,因为它们不改变所有权。
注意修改操作 :涉及修改操作的函数(如向集合中添加元素或删除元素)可能导致 move。例如,在调用 Vec::push 或 HashMap::insert 时,插入的元素会发生 move。
观察函数签名(重要) :检查函数的参数类型和返回类型。如果函数接受的是值类型(如 String、Vec 等)或返回值类型,那么调用此函数时可能涉及 move。相反,如果函数接受的是引用类型(如 &str、&[T] 等)或返回引用类型,那么调用此函数时通常不涉及 move。
实践和总结 :在实际编程过程中,多使用和理解这些函数。随着经验的积累,你会更好地理解哪些函数涉及 move,哪些不涉及。此外,可以尝试整理一份涉及和不涉及 move 的函数清单,以帮助记忆。
例如option unwrap方法,我们看它函数签名不涉及引用传递,说明会产生move
1 2 pub fn unwrap (self ) -> T;
第五章:移动
指针根据是否有所有权可以划分为两类,一类主要是智能指针,另外一类就是引用。rust中某个值的引用就是借用,之所以引出借用的概念是确保引用本身不能比借用的对象活得久。
值引用
掌握&和&mut的使用。ref可以被共享读,mut ref只能有一个对象持有进行读写
解引用
rust的引用不像c++,必须显示的创建引用,不过支持一些点操作符的隐式转换方便使用(不用像c++一样采用*解引用)。
集合里面的点操作实际上都支持自动解引用。方便对struct、集合里面的元素直接调用函数。
点操作符也支持对他的左操作符对象获取reference,比如声明一个向量,v.sort()其实等价于(&mut v).sort(),因为sort需要向量的可变引用,这边就做了自动转换。
1 2 3 4 5 6 fn main () { let mut v = vec! [1973 , 1968 ]; v.sort (); (&mut v).sort (); }
引用更新
rust引用因为是显式指定的,可以更换指向
c++隐式转换没法更换指向。参考p105
引用的引用
rust支持指向引用的引用,点符号支持自动多层解引用
引用的比较
rust==都是比较值(类型需要相同),比较地址用std::ptr::eq
rust引用的一些特点
引用永不为空 :表达类似空的概念可以用Option<&T>,需要强制检查更加安全
可以从任何表达式借用引用 :c、c++都是从变量获取引用,rust可以从任何表达式中借用引用。本质是创建一个持有表达式值的匿名变量,如果没有及时将引用赋值给变量,语句结束会自动释放
1 2 3 4 5 6 7 8 9 10 fn main () { let r = &factorial (6 ); assert_eq! (r + &1009 , 1729 ); } fn factorial (n: usize ) -> usize { (1 ..n + 1 ).product () }
胖指针
指向trait object和slice的是胖指针,包含额外信息比如元素个数。比较典型的胖指针主要是slice和trait的引用对象。
引用安全性
rust会为每个引用分配一个生命周期lifetime,来保障引用使用的安全性。
rust识别生命周期不正确的规则:
关于借用: x的引用的生命周期需要比出借方x的生命周期短。
关于引用赋值 : x的引用赋值给另外一个变量r,则x的引用生命周期必须小于r,也就是在r的生命周期内保持有效
生命周期本质是提供了一个scope信息,方便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 fn foo (x: & i32 , y: & i32 ) -> & i32 { if *x > *y { x } else { x } } #![allow(unused)] fn main () {fn longest (x: &str , y: &str ) -> &str { if x.len () > y.len () { x } else { y } } }
编译器可以自动推导生命周期的规则(也可以称之为三条消除规则),不满足则需要用户手动标注:
每一个引用参数都会获得自身独自的生命周期。例如2个参数的情况如下:
1 2 fn foo <'a , 'b >(x: &'a i32 , y: &'b i32 )
如果只有一个输入参数,所有引用类型的返回值的生命周期都与该输入相同
多个输入生命周期参数,但是包含&self或者&mut self,则返回值的生命周期会设置成和&self相同
用户手动标注的优先于编译器的自动标注,不过编译器仍然会检查合理性。例如返回引用的生命周期无法判断和&self的关系或者小于&self的生命周期(比&self活得久)就会报错,因为会产生悬垂引用。
通过move,比如返回非引用类型,可以避免陷入生命周期的处理中
结构体中包含引用的基本都没法自动推导(编译器无法自动推导结构体中引用的生命周期。这是因为结构体可能在很多不同的上下文和函数中使用,编译器无法预先知道它们的生命周期需求。),需要显式标注。结构体要求其中的引用活的比结构体自身更加久。参考例子如下。所以一般结构器中我们都使用String而不是&str slice从而避免陷入生命周期标注的问题中。
1 2 3 4 struct ImportantExcerpt <'a > { part: &'a str , }
函数的返回值是引用类型,他的生命周期必须比函数本身生命周期长,否则就产生悬垂引用了参考下面应用局部变量的例子:
1 2 3 4 5 6 7 8 9 #![allow(unused)] fn main () {fn longest <'a >(x: &str , y: &str ) -> &'a str { let result = String::from ("really long string" ); result.as_str () } }
为结构体实现方法时,注意匹配结构体中的生命周期标注(如果有的话)
1 2 3 4 5 6 7 8 9 10 11 12 13 #![allow(unused)] fn main () {struct ImportantExcerpt <'a > { part: &'a str , } impl <'a > ImportantExcerpt<'a > { fn level (&self ) -> i32 { 3 } } }
生命周期约束语法用于告诉编译器不同生命周期之间的关系(两种写法):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #![allow(unused)] fn main () {struct ImportantExcerpt <'a > { part: &'a str , } impl <'a : 'b , 'b > ImportantExcerpt<'a > { fn announce_and_return_part (&'a self , announcement: &'b str ) -> &'b str { println! ("Attention please: {}" , announcement); self .part } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #![allow(unused)] fn main () {impl <'a > ImportantExcerpt<'a > { fn announce_and_return_part <'b >(&'a self , announcement: &'b str ) -> &'b str where 'a : 'b , { println! ("Attention please: {}" , announcement); self .part } } }
特殊的静态生命周期 'static,表示该生命周期的引用可以和整个程序活的一样久
1 2 3 4 5 6 #![allow(unused)] fn main () {let s : &'static str = "我没啥优点,就是活得久,嘿嘿" ;}
Tips: 生命周期书本上后面章节有细说,但是此处我仍然提前拓展细化了下。这些内容来自于:rust圣经 和rust死灵书
第六章:表达式
表达式语言
区别表达式和语句:表达式(Expression)是指由变量、常量、运算符、函数调用等组成的代码片段,它们可以计算出一个值。而语句(Statement)是指一段代码,它会执行一些操作,但是不会返回一个值。
rust是一种表达式语言,即表达式完成所有工作。不像C++、Java等,rust中if、switch可以产生值,因此不需要引入类似java中的三元表达式 (expr1?expr2:expr3)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let status = if cpu.temperature <= MAX_TEMP { HttpStatus::Ok } else { HttpStatus::ServerError }; println! ( "Inside the vat, you see {}." , match vat.contents { Some (brain) => brain.desc (), None => "nothing of interest" , } );
运算符优先级
一般还是避免写复杂的搞不清优先级的长表达式。注意下字段、方法函数调用的优先级都是比比较运算符优先级高的,例如:limit < 2 * broom.size + 1 优先执行broom.size
代码块与分号
rust中分号结尾的话就会把表达式转化成语句,吞掉返回值;所以rust中经常结束的时候不加分号,表达式作为一个返回值直接返回即可
声明
let可以先声明不初始化,等后面初始化,不过编译器会检查是否使用未初始化的变量
非mut的只能初始化一次
同一个变量允许重复赋值,即使是不同的类型也是可以的
if和match
rust中的switch是比较强大的,最大的能力就是解构,这样可以方便的做参数匹配,另外由于rust是表达式语言,match中灵活使用各种表达式都很方便
if和match 分支的返回值类型必须相同
match的简化写法if let表达式 :用来match特定关系的表达式很有用,例如:
1 2 3 4 5 6 if let pattern = expr { } else { }
循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 while condition { } while let pattern = expr { } loop { } for pattern in itertable { }
主要for in 的写法很容易写错导致所有权转移,注意使用引用
1 2 3 4 5 6 7 8 9 10 11 12 13 for rs in &strings { println! ("String {:?} is at address {:p}." , *rs, rs); } fn main () { let mut strings = vec! ["hello" .to_string (), "world" .to_string ()]; for s in &mut strings { s.push_str ("\n" ); println! ("{}" , s); } println! ("{} error(s)" , strings.len ()); }
break和continue和别的语言用啊差不多,不过由于rust是表达式语言,break后面可以接表达式来代表loop表达式的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let answer = loop { if let Some (line) = next_line () { if line.starts_with ("answer: " ) { break line; } } else { break "answer: nothing" ; } };
never类型!
有些函数是死循环或者exit、panic!(),这种无法确定返回类型的可以用never类型,就是一个感叹号,例如标准库std:process::exit()中的实现:
1 2 3 4 5 pub fn exit (code: i32 ) -> ! { crate::rt::cleanup (); crate::sys::os::exit (code) }
函数和方法调用
引用类型和值类型是不同的类型,例如i32和&i32是不同的,不能自动转换
注意点符号在rust中具有自动解引用的功能,所以例如player.location中player可以是Player、&Plaryer、Rc等。
类型方法可以用双冒号,相当于静态方法使用,例如Vec::new。实现类型方法的话,定义结构体关联函数的时候不指定self参数即可
rust的函数和方法调用中不支持写明泛型,只能让rust自己推断
1 2 3 4 5 6 7 8 9 10 return Vec::<i32 >::with_capacity (1000 ); let ramp = (0 .. n).collect::<Vec <i32 >>(); return Vec::with_capacity (10 ); let ramp : Vec <i32 > = (0 .. n).collect ();
字段和索引
1 2 3 4 5 6 7 8 9 10 11 12 corrds.1 pieces[i] .. a .. .. b a .. b ..= b a ..= b
解引用操作符
主要就是*,不过由于.有自动解引用,这个星号用的比较少了
赋值、类型转换
不支持链式赋值a=b=3
基础类型转换需要显式使用as,例如内建数字类型、bool和char转整数类型(反之不然)
1 2 3 let x = 17 ; let index = x as usize ;
以下涉及引用类型的转换支持自动类型转换:
String 类型的值可以自动转换为 &str 类型;
&Vec 类型的值可以自动转换为 &[i32] 类型;
&Box 类型的值可以自动转换为 &Chessboard 类型;
第七章:错误处理
panic!
rust中panic!专门用于表示严重错误,程序会直接退出。相比java基于try-catch的异常设计,rust基于panic!和Result使用上会更加简单。通过传递Result和java try-catch基本上可以达到一样的效果。只不过result是强制程序必须处理,代码上安全性会更好一些,相当于异常处理更加显式化了
result
处理异常本质就是处理Result,可以用match或者result内建的一些方法:
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 result.is_ok (), result.is_err ():返回一个 bool 表示执行成功还是遇到错误; result.ok ():以 Option (T) 的形式返回成功值,如果结果是成功的则返回 Some (success_value),否则返回 None ; result.err ():以 Option (T) 的返回错误值; result.unwrap_or (fallback):如果有的话返回成功值,否则返回备选值,丢掉错误; const THE_USUAL: WeatherReport = WeatherReport::Sunny (72 );let report = get_weather (los_angeles).unwrap_or (THE_USUAL);display_weather (los_angeles, &report);result.unwrap_or_else (fallback_fn) let report = get_weather (hometown) .unwrap_or_else (|_err| vague_prediction (hometown)); result.unwrap () result.expect (message) result.as_ref () result.as_mut ()
result别名
这个比较常用,例如多个函数返回的类型都是Result<T, MyError>,我们可以定义一个Result别名,来省略每次都写MyError
1 2 3 4 5 pub type Result <T> = result::Result <T, MyError>;pub fn remove_file <P: AsRef <Path>>(path: P) -> Result <()>
错误打印
std::error::Error都有实现以下接口:
println!(),支持{}或{:?}打印内容
err.to_string()
err.source: 返回上层错误信息,如果要看最根源的root cause,得循环遍历err.source直到结束:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 use std::error::Error;use std::io::{Write, stderr};fn print_error (mut err: &dyn Error) { let _ = writeln! (stderr (), "error: {}" , err); while let Some (source) = err.source () { let _ = writeln! (stderr (), "caused by: {}" , source); err = source; } }
错误传播
针对result返回结果的表达式,最后用?可以在遇到错误时传播错误。效果类似于java在方法后面跟个throws Exception
传播任务类型错误
为了避免传播的错误与返回值不匹配,一般是配合dyn Error表示任意错误。一般可以使用Box<dyn std::error::Error + Send + Sync + 'static>类型表示所有错误:
dyn std::error::Error:表示任意错误;
Send + Sync + 'static:能够在多线程之间安全传递;
下面是一个使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use std::io::{self , BufRead};type GenericError = Box <dyn std::error::Error + Send + Sync + 'static >;type GenericResult <T> = Result <T, GenericError>;fn read_numbers (file: &mut dyn BufRead) -> GenericResult<Vec <i64 >> { let mut numbers = vec! []; for line_result in file.lines () { let line = line_result?; numbers.push (line.parse ()?); } Ok (numbers) }
处理特定类型错误
使用error内建的downcast_ref::()获取特定的类型来进行处理,参考例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 loop { match compile_project () { Ok (()) => return Ok (()), Err (err) => { if let Some (mse) = err.downcast_ref::<MissingSemicolonError>() { insert_semicolon_in_source_code (mse.file (), mse.line ())?; continue ; } return Err (err); } } }
另外可以考虑使用一些三方的异常处理库,例如thiserror 等
忽略错误
写到日志stderr即可
1 2 3 let _ = writeln! (stderr (), "error: {}" , err);
处理main函数中错误
main函数中错误没法使用?来继续传播了,已经是最外层了。一般是配合expect使用,报错就panic!并且打印一些信息:
1 2 3 4 fn main () { calculate_tides ().expect ("error" ); }
自定义错误
自定义错误需要定义结构体还要实现Display、Error等特性,比较麻烦,一般使用thiserror来实现:
1 2 3 4 5 6 7 8 9 use thiserror::Error;#[derive(Error, Debug)] #[error("{message:} ({line:}, {column})" )] pub struct JsonError { message: String , line: usize , column: usize , }
crates和module
crates和mod基础
rust按照crate组织独立的代码单元,编译的时候按照crate由编译器生成rlib中间代码文件,然后再进行静态库的编译和链接。
edition相当于rust对改动特别大的版本设计的namespace,例如edition 2018引入了async、await关键字。注意不是每年都有新edition的
cargo.toml中可以按照profile指定配置,不指定的话运行就都使用同一套配置。配置分为dev、release、test和bench。这些profile和rust编译器的工作机制都是相关的,需要注意。例如release的时候就会少做一些数值溢出检查来提升编译性能。
模块两种定义方式,一种定义在mod.rs内,一种定义在以module name命名的文件内。例如写一个mod spores,rust会寻找spores.rs或者spores/mod.rs。mod写好了,注意在main.rs或lib.rs中声明
模块、方法可见性
导入
导入 :类似java import,通过use 来简化使用,导入的时候可以用self、supper等来表示模块自身和父模块
预导入 :std::prelude中的内容都是预导入的,可以直接用
导出与可见性
模块、方法导出 :pub mod表示对别的crate可见(pub(super)表示对父模块可见);mod当中的函数加pub表示对其他mod可见;
别名导出 :把别的模块的内容(需要自己可见)作为自己的一部分进行公共导出,使用pub use
1 2 3 4 pub use self::leaves::Leaf;pub use self::roots::Root;
结构体导出 :结构体类型、字段都可以导出,使得模块外可以被使用
1 2 3 4 5 6 pub struct Fern { pub roots: RootSet, pub stems: StemSet }
1 2 3 pub const ROOM_TEMPERATURE: f64 = 20.0 ; pub static ROOM_TEMPERATURE: f64 = 68.0 ;
lib和bin项目
一个crate可以同时构建lib和bin,下面是一个例子
1 2 3 4 5 6 7 8 9 10 11 ├── Cargo.lock ├── Cargo.toml ├── package-lock.json ├── package.json └── src ├── bin | ├── mandelbrot.rs | └── mandelbrot_v2 | └── main.rs └── lib.rs
属性
在 Rust 中,属性是一种特殊的注释,可以用来控制编译器、文档生成工具等工具的行为。属性使用 #[ ] 的形式来标识,可以放在函数、结构体、枚举、模块等代码块的前面。支持的属性可以看rust参考手册:[条件编译](属性使用 #[ ] 的形式来标识,可以放在函数、结构体、枚举、模块等代码块的前面。)
#[derive]是比较常用的属性,让结构体、枚举自动生成实现特定trait的代码
#[test]用于标记测试函数
区别于trait,自定义属性主要为代码添加元数据,并在编译时进行检查。常用的包括代码检查、性能分析、代码覆盖率测试等
整个crate级别的属性需要附加到main.rs或lib.rs的顶部,写成#!,例如
1 #![allow(non_camel_case_types)]
测试
单元测试:rust的工具链比较好,内置了单元测试框架,配合#[test]即可执行单元测试
集成测试:在src目录下新建一个tests目录,实现的集成测试代码放里面,将我们自己的crate当做一个外部依赖mod进行测试即可:
1 2 3 4 5 6 7 8 9 10 11 12 use fern_sim::Terrarium;use std::time::Duration;#[test] fn test_fiddlehead_unfurling () { let mut world = Terrarium::load ("tests/unfurl_files/fiddlehead.tm" ); assert! (world.fern (0 ).is_furled ()); let one_hour = Duration::from_secs (60 * 60 ); world.apply_sunlight (one_hour); assert! (world.fern (0 ).is_fully_unfurled ()); }
文档
/// 表示文档祖师
//! 用于模块或者crate的注释
rust注释可以用rust模块组织路径去引用rust定义的函数。中括号括起来即可。
1 2 3 4 5 6 7 8 pub fn trace_path (leaf: &leaves::Leaf, root: &roots::Root) -> VascularPath { ... }
1 2 3 4 5 #[doc(alias = "route" )] pub struct VascularPath { ... }
注释可以内嵌代码,支持markdown格式。文档中涉及的代码块编译器会把他们弄成一个独立的crate编译检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pub fn upload_all (&mut self ) { ... }
依赖声明
cargo.lock是用于针对范围版本声明的依赖锁版本信息的,避免每次build去使用范围中最新的版本
工作区间
默认不同crate的构建编译都是完全独立的,可以cargo.toml中指定member来共享构建空间
1 2 [workspace] members = ["fern_sim" , "fern_img" , "fern_video" ]
第八章:结构体
基础知识
使用两个点用已有结构体中的变量值来完成当前新结构体的初始化
1 2 3 4 5 6 7 8 9 10 let p1 = Person { name: "michael" .to_string (), age: 28 , sex: '男' , }; let p2 = Person { name: "skye" .to_string (), ..p1 };
元组结构体,相当于对元组做了命名。可以类似结构体的方式导出,用法上也相同
1 2 struct Bounds (usize , usize );
unit结构体:例如 struct PlaceHoulder 就是一个单元结构体,一般用于占位符,不占用任何内存资源。这个就比java好一些。设置一个枚举当占位符会有额外内存开销,而且定义起来也麻烦。
内存布局
rust结构体默认情况下直接放在栈帧中,而不是堆上。下面是GrayScaleMap结构体的内存布局:
1 2 3 4 5 struct GrayscaleMap { pixels: Vec <u8 >, size: (usize , usize ) }
和C、C++的区别:rust对内存中字段排序不作出承诺,可能因为内存对齐进行重排序。可以通过#[repr©]来禁止结构体字段重排序
和Java的区别:java的类和结构体是两样东西,数据都放堆上的
实例方法
通过impl块来定义结构体的关联函数。如果没有定义在impl块中的,称之为自由函数
关联函数可以指定self,根据是否可变、是否获取所有权有如下写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 impl Queue { pub fn is_empty (&self ) -> bool { self .older.is_empty () && self .younger.is_empty () } } impl Queue { pub fn is_empty (&mut self ) -> bool { self .older.is_empty () && self .younger.is_empty () } } impl Queue { pub fn split (self ) -> (Vec <char >, Vec <char >) { (self .older, self .younger) } }
智能指针
关联函数当中包含的self可以有多种写法,其中可以使用一些智能指针,如下。
1 2 3 4 5 6 7 8 9 impl Queue { pub fn split (&self : Rc<Self >) -> (Vec <char >, Vec <char >) { (self .older, self .younger) } } let shared_queue = Rc::new (Queue::new ());
类型方法
其实就是静态方法,定义结构体关联放的时候,不指定self即可
1 2 3 4 5 6 impl Queue { pub fn new () -> Queue { Queue { older: Vec::new (), younger: Vec::new () } } }
关联常量
结构体关联的常量,和类型关联方法采用一样的调用方式,即双冒号调用
1 2 3 4 5 6 7 8 9 10 11 12 pub struct Vector2 { x: f32 , y: f32 , } impl Vector2 { const ZERO: Vector2 = Vector2 { x: 0.0 , y: 0.0 }; const UNIT: Vector2 = Vector2 { x: 1.0 , y: 0.0 }; } let scaled = Vector2::UNIT.scaled_by (2.0 );
泛型结构体
这里需要注意的是,虽然声明了泛型结构体和关联方法,仍然可以针对特定类型实现一些特定的关联方法:
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 impl <T> Queue<T> { pub fn new () -> Queue<T> { Queue { older: Vec::new (), younger: Vec::new () } } pub fn push (&mut self , t: T) { self .younger.push (t); } pub fn is_empty (&self ) -> bool { self .older.is_empty () && self .younger.is_empty () } ... } impl Queue <f64 > { fn sum (&self ) -> f64 { ... } }
常用派生
自定义结构体如果要支持debug打印、比较、深拷贝就得实现Copy、Debug、PartialEq这些trait。rust内建属性派生可以在编译的时候帮我们自动实现(前提是结构体内部的所有字段本身需要已经实现了这些派生trait,否则会执行报错)
内部可变性(Cell和RefCell)
所谓的内部可变性,指的是针对不可变引用内部进行修改的能力。比如一个不可变引用&self,我们希望对其内部字段进行修改,就需要内部可见性的能力。
Cell
Cell实现方式:Set新对象,然后丢弃旧对象,一般用于实现计数器。实现上非常轻量,不影响性能。
限制:
内部容纳的元素需要实现Copy trait
线程不安全
核心作用总结:声明后,对Cell容器内的内部字段进行修改(即使涉及的是不可变对象,也依然可以)
使用场景总结:
细粒度暴露可变字段 :Cell的出现在很多时候是方便我们细粒度 地去暴露一个不可变引用中的可变字段。
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 pub struct SpiderRobot { species: String , web_enabled: bool , leg_devices: [fd::FileDesc; 8 ], ... } pub struct SpiderSenses {robot: Rc<SpiderRobot>, motion: Accelerometer, ... } pub struct SpiderRobot { hardware_error_count: Cell<u32 >, } pub fn add_hardware_error (&self ) { let n = self .hardware_error_count.get (); self .hardware_error_count.set (n + 1 ); }
用后即丢的场景 :一些对象本身设计上就是不可变的,但是一些使用这些不可变对象的时候需要修改值,例如&str类型。其实我们自己也很容易实现满足Cell功能的代码,其实就是以新换旧,只不过用Cell更加像直接使用一个语法糖,更加的简单。
1 2 3 4 5 6 7 8 9 use std::cell::Cell;fn main () { let c = Cell::new ("asdf" ); let one = c.get (); c.set ("qwer" ); let two = c.get (); println! ("{},{}" , one, two); }
RefCell
Cell的限制就是必须是实现Copy trait的对象,如果没有实现Copy trait则不可以使用。这些没实现Copy trait的对象可以用RefCell。
实现方式:RefCell维护一个借用检查器,运行时进行借用规则检查。相比Cell性能有一些开销,但是从语言层面来说开销还是不大的。参考Rust语言圣经 相关章节。
限制:内部容纳元素无需实现Copy trait;仍然不是线程安全的
核心作用:对RefCell容器内的元素进行运行时可变性的修改,即使是不可变的引用,也仍然可以修改(不过不能违反借用规则,比如不能违反同时只有一个人可以修改)。
使用场景:和cell的场景类似,只不过额外支持非Copy的对象
参考例子:
1 2 3 4 5 6 7 impl SpiderRobot {pub fn log (&self , message: &str ) { let mut file = self .log_file.borrow_mut (); } }
rc和refcell配合使用
比较常用的组合是rc实现引用共享,refcell实现内部可变性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 use std::cell::RefCell;use std::rc::Rc;fn main () { let s = Rc::new (RefCell::new ("我很善变,还拥有多个主人" .to_string ())); let s1 = s.clone (); let s2 = s.clone (); s2.borrow_mut ().push_str (", on yeah!" ); println! ("{:?}\n{:?}\n{:?}" , s, s1, s2); }
第十章: 枚举和模式匹配
枚举
枚举存储 :枚举在内存中用一个u8表示,序数可以手动指定,但是序数范围只能是[0,255],表示一个u8的范围。
1 2 3 4 5 6 7 8 enum HttpStatus { Ok = 200 , NotModified = 304 , NotFound = 404 , ... }
枚举不可以直接用序数访问,枚举可以转换成u8,但是不能把u8强制转换成枚举
rust枚举相比别的语言强大的多,是因为rust枚举可以包含方法
1 2 3 4 5 6 7 8 9 #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum TimeUnit { Seconds, Minutes, Hours, Days, Months, Years, } impl TimeUnit { }
rust枚举可以携带参数,可以用来创建枚举。枚举定义类型分三种:
不携带参数,和别的语言中枚举比较像
携带参数,tuple风格
携带参数,结构体风格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 enum RoughTime { InThePast (TimeUnit, u32 ), JustNow, InTheFuture (TimeUnit, u32 ), } let four_score_and_seven_years_ago = RoughTime::InThePast (TimeUnit::Years, 4 * 20 + 7 );let three_hours_from_now = RoughTime::InTheFuture (TimeUnit::Hours, 3 );enum Shape { Sphere { center: Point3d, radius: f32 }, Cuboid { corner1: Point3d, corner2: Point3d }, } let unit_sphere = Shape::Sphere { center: ORIGIN, radius: 1.0 , };
带有数据的枚举,内存表示(以前文提到的2个RoughTime实例为例):
tag字段rust内部使用,指出哪个构造函数创建了该值以及它具有哪些字段
类似结构体,rust对枚举内存布局也不做承诺,重排序都是可能的。
书中有一个标识Json的枚举(富数据结构)示例如下定义,核心是告诉我们一个最佳实践:不要在枚举参数中引入大对象,可以使用智能指针
1 2 3 4 5 6 7 8 9 10 11 use std::collections::HashMap;enum Json { Null, Boolean (bool ), Number (f64 ), String (String ), Array (Vec <Json>), Object (Box <HashMap<String , Json>>), }
由于rust枚举支持参数,枚举也是可以支持泛型的.
1 2 3 4 5 6 7 8 9 10 11 12 enum Option <T> { None , Some (T), } enum Result <T, E> { Ok (T), Err (E), }
枚举中要访问枚举中的数据,注意需要使用模式匹配
rust枚举的理念是富数据结构,某种意义上符合领域模型的涉及,把枚举以及枚举关联的参数都包含在一起,作为一个整体使用,这也使得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 match meadow.count_rabbits () { 0 => {} 1 => println! ("A rabbit is nosing around in the clover." ), n => println! ("There are {} rabbits hopping about in the meadow" , n), } let calendar = match settings.get_string ("calendar" ) { "gregorian" => Calendar::Gregorian, "chinese" => Calendar::Chinese, "ethiopian" => Calendar::Ethiopian, other => return parse_error ("calendar" , other), }; let caption = match photo.tagged_pet () { Pet::Tyrannosaur => "RRRAAAAAHHHHHH" , Pet::Samoyed => "*dog thoughts*" , _ => "I'm cute, love me" , }; fn describe_point (x: i32 , y: i32 ) -> &'static str { use std::cmp::Ordering::*; match (x.cmp (&0 ), y.cmp (&0 )) { (Equal, Equal) => "at the origin" , (_, Equal) => "on the x axis" , (Equal, _) => "on the y axis" , (Greater, Greater) => "in the first quadrant" , (Less, Greater) => "in the second quadrant" , _ => "somewhere else" , } } match balloon.location { Point { x: 0 , y: height } => println! ("straight up {} meters" , height), Point { x,y } => println! ("at ({}m, {}m)" , x, y), } Some (Account { name, language, .. }) => language.show_custom_greeting (name),
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn hsl_to_rgb (hsl: [u8 ; 3 ]) -> [u8 ; 3 ] { match hsl { [_, _, 0 ] => [0 , 0 , 0 ], [_, _, 255 ] => [255 , 255 , 255 ], ... } } fn greet_people (names: &[&str ]) { match names { [] => { println! ("Hello, nobody." ) }, [a] => { println! ("Hello, {}." , a) }, [a, b] => { println! ("Hello, {} and {}." , a, b) }, [a, .., b] => { println! ("Hello, everyone from {} to {}." , a, b) } } }
匹配与所有权转移
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 match account { Account { name, language, .. } => { ui.greet (&name, &language); ui.show_settings (&account); } } match account { Account { ref name, ref language, .. } => { ui.greet (name, language); ui.show_settings (&account); } } match &account { Account { name, language, .. } => { ui.greet (name, language); ui.show_settings (&account); }
注意结构体中的引用必须使用ref而不是使用&。&智能用于获取一个变量的引用
match匹配结构体的应用,如果要修改就使用ref mut
1 2 3 4 5 6 7 8 match line_result { Err (ref err) => log_error (err), Ok (ref mut line) => { trim_comments (line); handle (line); } }
模式和表达式作用是不同的,一个解构一个组成新值。在表达式中,& 创建一个引用,在一个模式中, & 匹配一个引用
条件模式
模式匹配可以配合if条件使用
1 2 3 4 5 6 7 match point_to_hex (click) { None => Err ("That's not a game space." ), Some (hex) if hex == current_hex => Err ("You are already there! You must click somewhere else" ), Some (hex) => Ok (hex) }
匹配多种可能
1 2 3 4 5 let at_end = match chars.peek () { Some (&'\r' ) | Some (&'\n' ) | None => true , _ => false , };
@绑定
@ 符号是个绑定符号,将match的匹配项绑定到一个新名字方便使用。好处就是你为了复用变量时不需要重新定义一遍。注意绑定的时候,如果是非copy类型的对象,涉及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 27 match self .get_selection () { Shape::Rect (top_left, bottom_right) => { optimized_paint (&Shape::Rect (top_left, bottom_right)) } other_shape => { paint_outline (other_shape.get_outline ()) } } match self .get_selection () { rect @ Shape::Rect (top_left, bottom_right) => { optimized_paint (&rect) } other_shape => { paint_outline (other_shape.get_outline ()) } } match chars.next () { Some (digit @ '0' ..='9' ) => read_number (digit, chars), ... },
模式用于解构以及配合if let和while let
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 let Track { album, track_number, title, .. } = song;fn distance_to ((x, y): (f64 , f64 )) -> f64 { ... }for (id, document) in &cache_map { println! ("Document #{}: {}" , id, document.title); } let sum = numbers.fold (0 , |a, &num| a + num);if let RoughTime ::InTheFuture (_, _) = user.date_of_birth () { user.set_time_traveler (true ); } if let Some (document) = cache_map.get (&id) { return send_cached_response (document); } while let Err (err) = present_cheesy_anti_robot_task () { log_robot_attempt (err); } while let Some (_) = lines.peek () { read_paragraph (&mut lines); }
第十一章: Traits和泛型
trait
trait基础
dyn动态分派,dyn 后面加个trait,表示任何实现了该trait的对象。动态分配有一定性能开销,需留意,不过为了可读性还是可以考虑使用的。
具体对象没有办法赋值和转化成dyn trait对象,可以使用引用,因为引用的值在编译时即确定好的
Trait 对象是一个胖指针(同理的还有Box),由一个指向值的指针和一个指向拥有该值类型方法表的指针组成,因此,每个 Trait 对象占用两个机器字(32位4字节,64位8字节)
因为要在编译期确定大小,使用dyn trait的函数只能接收&dyn T或者Box类型。
即使是已经写好代码类型,也可以给它附加trait。rust的trait分离了定义和绑定,不像java必须在一个类定义的时候实现好接口。下面是rust为内置char类型新绑定一个trait的代码:
1 2 3 4 5 6 7 8 9 10 11 trait IsEmoji { fn is_emoji (&self ) -> bool ; } impl IsEmoji for char { fn is_emoji (&self ) -> bool { ... } }
可以为某一类泛型实现trait。可见rust trait的设计充分考虑了组合优于继承的理念,算是新语言的后发优势吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 use std::io::{self , Write};trait WriteHtml { fn write_html (&mut self , html: &HtmlDocument) -> io::Result <()>; } impl <W: Write> WriteHtml for W { fn write_html (&mut self , html: &HtmlDocument) -> io::Result <()> { ... } }
trait的关联函数可以包含构造函数、静态方法等,比较自由
rust会根据调用主体自动查找方法匹配,比如to_string会自动查找调用ToString trait的方法
trait本身也可以是泛型的
trait对象动态分派的安全性
牢记trait对象会涉及动态分派。如果需要使用动态分派的trait对象(表示为&dyn T或Box),为了保证涉及动态分发时trait对象的安全,定义trait我们可以提前遵循一些的原则,这样确保未来可以放心使用动态分派:
该 trait 中的所有方法都必须只接受 &self 或 &mut self 作为第一个参数 :只使用引用,因为引用的大小在编译时是可以确定的
该 trait 中的所有方法的返回值不能包含Self类型(返回Self的叫做类型关联函数): 返回Self类型不正确,因为我们不知道具体运行时的类型,一般返回一个Box对象。
高性能场景多用静态分派trait
高性能场景尽量用静态分派的trait,一般诀窍就是确保代码能够在编译时就确定类型。一般trait配合有约束的泛型以及合理的调用,可以在编译时确定好类型,这样就是静态分派了。参考资料[3]中也提到了tikv中使用动态分发、Box导致
子trait
trait上引入的一种继承机制,要求必须实现父trait,用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 trait Creature where Self : Visible { ... } trait Creature : Visible { fn position (&self ) -> (i32 , i32 ); fn facing (&self ) -> Direction; ... }
trait关联类型
就是可以让使用者自定义trait中的type具体是什么类型,这种方式和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 impl Iterator for Args { type Item = String ; fn next (&mut self ) -> Option <String > { self .inner.next ().map (|s| s.into_string ().unwrap ()) } ... } fn collect_into_vector <I: Iterator >(iter: I) -> Vec <I::Item> { let mut results = Vec::new (); for value in iter { results.push (value); } results } fn dump <I>(iter: I) where I: Iterator <Item=String > { for (index, value) in iter.enumerate () { println! ("{}: {:?}" , index, value); } }
泛型
基础知识
1 2 3 4 5 6 let v1 = (0 .. 1000 ).collect (); let v2 = (0 .. 1000 ).collect::<Vec <i32 >>(); let v3 : Vec <i32 > = (0 ..1000 ).collect ();
多个泛型参数,涉及类型组合的,可以配合where使用。
1 2 3 4 5 6 7 8 fn run_query <M, R>(data: &DataSet, map: M, reduce: R) -> Resultswhere M: Mapper + Serialize, R: Reducer + Serialize, { ... }
泛型函数的参数涉及引用的一般需要标注生命周期,生命周期需要写最前面:
1 2 3 4 5 6 7 8 9 fn nearest <'t , 'c , P>(target: &'t P, candidates: &'c [P]) -> &'c Pwhere P: MeasureDistance, { ... }
结构体不是泛型,函数依然是可以泛型的(这个比Java灵活多了)
trait vs 泛型 :泛型是编译时确定的,trait需要运行时确定。泛型主要还是在于处理功能类似只是类型不同的场景,trait则是封装了一组能力
impl trait返回匿名类型
功能类似java泛型extends的效果,名字稍微有点误导。不过没有类似java super的写法,其他场景有where和加号组合即可。一般用在参数中,返回值不允许用,返回trait对象没意义,得返回Box对象。impl trait在参数和泛型之间有关联时,没法使用,需留意,见Tips
1 2 3 4 5 6 7 8 fn print <T: Display>(val: T) { println! ("{}" , val); } fn print (val: impl Display ) { println! ("{}" , val); }
tips: 有一个重要的例外,使用泛型函数允许函数调用者声明泛型参数类型,例如:print::(42),但是当使用 impl Trait 是不允许的。
每个 impl Trait 参数都分配有自己的匿名类型参数,因此参数是 impl Trait 仅限于简单的泛型函数,类型和参数之间没有关系的。
第十二章:操作符重载
了解rust的操作符都是通过trait实现的,例如a+b实际上是a.add(b)。可以重载的运算符总结如下:
大部分操作符重载都在std::ops下,注意算术运算符会转移所有权,比较操作符不会,具体可以看方法签名
有些操作符重载不需要自己每次都写,可以用派生,例如partialEq默认实现的就是比较所有字段
index和indexMut这两个trait用来完成索引运算符[]的重载
第十三章:常用Trait
常用trait分类
语言扩展 Trait :主要用于运算符重载,我们可以将常用的运算符使用在自己的类型之中,只要相应的 Trait 即可,例如 Eq,AddAssign,Dere,Drop 以及 From 和 Into 等;
标记类型 Trait :这些 Trait 主要用于绑定泛型类型变量,以表达无法以其他方式捕获的约束,这些包括 Sized 和 Copy;
剩下的主要是一些为解决常遇到的问题 ,例如:Default,AsRef,AsMut,Borrow,BorrowMut,TryFrom 和 TryInto;
Drop
相当于C++析构函数,清理对象。只要实现了drop trait,rust会负责自动清理对象,采用RAII机制 。
rust会帮助自动实现trait,除非存在它不支持的情况。不支持的
copy trait的实现对象
裸指针:因为不受rust所有权制约,也就无法控制在其声明周期结束的时候释放内存
循环引用的场景:比如结构体内字段互相引用,无法确定字段释放的顺序
实现了deref trait的类型
drop只能被隐式调用
drop调用发生在如下时机。总结的话就是值生命周期结束的时候,具体场景包括:
值离开作用域
当可变变量重新被赋予新值
表达式生命周期结束,无效时
函数传递值并且生命周期结束
Sized
表示确定大小的,意味着实现该trait的对象,都是可以在编译器确定大小的。这个trait不需要我们自己实现,rust都会自动实现。
Sized trait在标注泛型边界的时候比较有用(默认rust也是会自动为泛型加Sized),从而避免使用动态分派的对象。
编译器不可以确定大小的类型有:
trait对象 dyn T
切片slice,例如&str、[T]等
结构体的最后一个字段可以是unsized,可以看RcBox这个内部实现Rc的对象
Clone
Clone有扩展Sized trait,区别于copy开销较小的栈上复制,clone是重度的深拷贝。除了一些智能指针(例如Rc和Arc)clone的开销较低外,大部分自定义对象需要注意使用clone的性能开销
clone不是move,而是创建深拷贝的新对象以及新的所有权。老的对象和所有权依然有效。
Copy
copy和clone不太一样,采用按位复制(字节复制),是一种栈上复制,开销比较小。
复制一个新对象,老的对象仍然保留值和其所有权
Deref、DerefMut
自动解引用是为了方便调用,主要以下几种场景:
智能指针自动解引用 :例如Box和Rc都可以通过点进行自动解引用调用
slice和关联的结构体对象之间的转换 :主要是String和&str、&Vec和&[T]之间的转化,并且支持连续转换。例如&Rc可以转换成&String再到&str
解引用配合泛型函数使用的时候不会自动解引用 来判断是否满足泛型约束,需要手动解引用
1 2 3 4 5 6 7 8 9 10 11 12 13 fn show_it_generic <T: Display>(thing: T) { println! ("{}" , thing); } fn main () { let s = Selector { elements: vec! ["good" , "bad" , "ugly" ], current: 2 , }; show_it_generic (&s); }
Default
Default trait用来给类型提供默认值
rust的集合类型都实现了Default,返回空的集合
如果T实现了Default,rust会为Rc等智能指针自动实现Default
tuple中所有元素都实现Default,则tuple也会自动实现
结构体不会自动实现Default,如果所有字段都实现Default,也需要显式派生#[derive(Default)]
AsRef、AsMut
函数传参的时候方便传递引用,&T和&mut T可以自动调用as_ref和as_mut。实现该trait就可以使用语法糖&T和&mut T了。
rust标准库的类型大部分都实现了asref和asmut,除了基础类型、指针、单元类型等一些特别的类型
copy类型也可以实现asref、asmut不冲突,copy关键是实现位复制
Borrow、BorrowMut
通过传递引用即可完成借用,即使没有实现borrow trait也可以借用
borrow trait主要用于代码层面支持更多类型的借用参数的传递。例如HashMap集合,传递的参数如果是Borrow类型的,那么他就可以同时接收&str、&String等不同类型的参数(String实现了Borrow和Borrow),要不然的话只能严格限制函数接收&str或者&String
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 impl <K, V> HashMap<K, V> where K: Eq + Hash{ fn get (&self , key: &K) -> Option <&V> { ... } } impl <K, V> HashMap<K, V> where K: Eq + Hash{ fn get <Q: ?Sized >(&self , key: &Q) -> Option <&V> where K: Borrow<Q>, Q: Eq + Hash { ... } }
borrow trait有一个约束:一个类型应该实现 Borrow 只有当 &T 的 hash 和它借用的值的 hash 相同时
From、Into
会获取值的所有权,并且转化为另外一个类型
from和into实现一种则另外一种自动实现,本质就是从别的类型对象构造自己
1 let addr1 = Ipv4Addr::from ([66 , 146 , 219 , 98 ]);
TryFrom、TryInto
和from、into功能相同,只不过需要处理失败的情况
from的值来源不可控的情况,比如出现非法值导致from失败的情况,可以用tryFrom
1 2 3 4 5 6 7 fn main () { let huge = 2_000_000_000_000i64 ; let smaller : i32 = huge.try_into ().unwrap_or (i32::MAX); println! ("{}" , smaller); }
ToOwned
使得使用者可以从一个引用类型创建一个新的出借者对象(老的引用仍然保留)。常用场景例如:从&str、&[i32]中创建出借者类型的对象String和Vec
Cow
cow即clone-on-write,顾名思义,意思是只有需要写的时候才clone,其他情况都不进行复制,用来减少内存的分配和复制,适合读多写少的对象。
Cow是个容器,既可以接收T也可以接受&T
Cow实现了Deref和Clone trait
关键方法理解:to_mut和into_owned都会进行判断,只有在当前对象和函数签名不匹配时才clone
to_mut(): 就是返回数据的可变引用,如果没有数据的所有权,则复制拥有后再返回可变引用;
into_owned(): 获取一个拥有所有权的对象(区别与引用),如果当前是借用,则发生复制,创建新的所有权对象,如果已拥有所有权,则转移至新对象。
本质作用理解 :核心是传递参数时可以用Cow来同时接收可变引用类型或者move的对象。这样传参的时候就不用涉及类型转换。具体函数实现的时候,在满足特定条件的时候我们可以使用to_mut或者to_owned来拿到对应的对象来处理,通过后置类型转换的时机、缩小类型转换发生的范围来避免内存复制的额外开销。
cow更像是一个语法糖,其实写两个重载方法,一个接收借用对象,一个接收拥有者对象也是一样的,不过使用cow代码更加简洁一些。
第十四章:闭包
变量捕获
捕获闭包外的变量,涉及如下知识点:
闭包的生命周期不能超过捕获变量的生命周期
闭包调用结束的时候释放捕获的变量
变量捕获的两种情况:借用和移动
捕获变量分为两种情况:
借用: 如果捕获的变量生命周期是大于闭包的,直接借用即可
move:如果捕获的变量采用借用的话生命周期不够长,需要使用move
闭包和函数类型
闭包和函数可以作为一个类型传递和赋值,函数类型可以用fn(&T)->T 类似的结构来表示,fn表示匿名函数,然后需要标识出入参和返回值类型
1 2 3 4 5 6 7 8 let key_fn = move |city: &City| -> i64 { -city.get_statistic (stat) };fn count_selected_cities (cities: &Vec <City>, test_fn: fn (&City) -> bool ) -> usize { ... }
闭包作为参数传递,函数签名必须用where,因为具体的函数类型fn(&City)->bool这样的没办法匹配闭包。(我理解是rust编译器这点上还存在不足)
闭包性能
rust的闭包支持内联来改善函数调用性能,不会有太大性能开销,可以放心用。闭包在内存上布局如下:
闭包和安全性
使用闭包主要考虑安全性问题,因为闭包重复使用,涉及对捕获值的重复使用,如果不特别处理,闭包只能使用一次,然后Drop掉捕获的变量。书中称为:杀死值的闭包。
闭包调用的时候实际上也会脱糖,变成调用:clousure.call()或者clousure.call_once()
理解三种函数类型trait: Fn、FnMut、FnOnce
这三种trait主要用于声明参数的函数类型
Fn: 可以重复使用函数或者闭包,捕获的变量不可变
FnMut :可以重复使用函数或者闭包,捕获的变量可变。意味着函数闭包签名一定要使用&mut。
FnOnce : 闭包和函数只能使用一次,一定会move变量,获得所有权,执行完毕后drop。意味着FnOnce肯定是配合move来捕获变量的 。
实现上来看,Fn 是 FnMut 的子 Trait,而 FnMut 又是 FnOnce 的子 Trait。FnOnce是获得所有权的,可以随意处置捕获的变量,相当于限制最少,所以是最高层的trait。这意味着:
需要Fn由于是最下面的子类型,所以传递给FnMut和FnOnce都是可以的。
Copy、Clone闭包
是否可以对闭包clone和copy取决于捕获变量的情况。
针对非move类型的闭包函数类型:只包含共享引用的闭包 是可以 Clone 和 Copy
1 2 3 4 5 let y = 10 ;let add_y = |x| x + y;let copy_of_add_y = add_y; assert_eq! (add_y (copy_of_add_y (22 )), 42 );
针对move类型,如果捕获的变量都是copy则为copy,都是clone则为clone
高效使用闭包
rust的所有权和生命周期在处理一些双向引用的场景会比较负载,建议使用单向无环的架构。例如用flux的架构而不是MVC,避免双向引用:
第十五章:迭代器
参考资料