九、集合
rust内置的一系列数据结构,区别于元组和数组,集合都是存在堆内存的。
9.1 vector
类似于数组,但是存入堆内存,所以大小是可变的。关键字是Vec
类型。
创建一个Vec
类型的值:
// 这里创建一个空的Vec类型的数据,需要是可变的, 后期可以增删数据
let mut list1: Vec<u8> = Vec::new();
在rust中,还提供了宏指令vec!
来快捷创建一个vec类型的数据。
let list2 = vec![1, 2, 3, 4];
// vec! 宏创建的值,会自动推测泛型的类型
增加元素
对于可变的vec类型实例,可以使用push
方法,给实例添加值:
let mut list1: Vec<u8> = Vec::new();
list1.push(1);
读取元素
读取vec类型数据的值,有两种方式:索引法、get
方法:
let mut list2 = vec![1, 2, 3, 4];
// 索引法
let first = & list2[0];
println!("{}", first);
// get 方法
if let Some(value) = list2.get(5) {
println ! ("value is {}", value);
}
在文档中,说不要在获取引用后,再修改值,会导致集合类型在堆中的地址发生变化,从而让导致原来的引用找不到值,但是 在实际中并没有发现会报错。
遍历vector
和读取元素的示例不同,遍历时,我们使用的是vec的引用。是因为在实际中,经常会发生移动的情况,所以一般 来说,操作集合类型都是使用它的引用。
let v = vec![1, 2, 3, 4, 5, 6, 7];
for i in & v {
println!("{}", i);
}
9.2 String类型
字符串分成两类, 核心语言中的str
类型,和标准库中的String
类型。
str
类型
核心语言部分,对字符串的定义是:
某处存储的二进制源,然后进行slice,返回&str作为str的引用(大意)
// 默认定义的字符串字面量,就是str类型,但是使用时,都是从二进制数据进行的slice,所以是&str
let s1: & str = "hello";
String
类型
String类型是标准库中定义的String
类型,基于Vec<u8>
。 它是内容可变的,使用utf-8编码, 标准库提供了多种方式用来初始化String
类型的实例,和修改实例的值:
// 初始化的方法
let mut s2 = String::new();
let mut s3 = String::from("world");
let s4 = "s4".to_string();
String类型是存储在堆上的,可以使用提供的方法,对其内容进行修改:
// push_str : 给字符串添加字符串
s2.push_str("yoyo");
// push : 给字符串,添加一个字符
s3.push('h');
// + 号,相当于 add 方法,第一个参数是self(必须是String类型),第二参数是引用
let s4 = s4 + & s2;
// 为了简化 + 号运算符的缺陷,可以使用 formate! 宏
let s4 = format!("{} - {}", s4, &s3); // formate 宏,不会获取参数的所有权
String类型的实例,不支持使用索引获取,但是可以使用内置方法,获取到字符串的每个字符,和每一位unicode码:
// 遍历
// 获取每个char类型的值:chars() 方法
for c in "123456".to_string().chars() {
println ! ("{}", c);
}
for c in "中国".to_string().bytes() {
println ! ("{}", c);
}
9.3 HashMap
用来存储键值对。键和值,都可以任何类型的。
// 构造一个空的HashMap
use std::collections::HashMap;
let mut map2 = HashMap::new();
在map实例创建后,可以对其进行插入值、修改值、查询值、删除值等操作:
// 首先定义一个key
let key_1 = "key-1".to_string();
// 给 hashmap 插入一个键值对,如果重复插入,会覆盖。
// 而且 insert 函数,会获取参数的使用权,会造成 移动
map2.insert( & key_1, 100);
// get 函数,获取map的值,返回是个Option , 需要使用 match进行匹配处理
if let Some(value) = map2.get( & key_1) {
println ! ("{}", value);
} else {
println ! ("map2 中没有这个值");
}
// hashmap可以使用for..in..进行迭代
for (k, v) in & map2 {
println!("{} : {}", k, v);
}
// 判断键是否存在,并传入一个值
let entry_key = "entry_key".to_string();
let entry_value = map2.entry( & entry_key).or_insert(90);
println!("{}", entry_value);
十、异常处理
10.1 panic!
penic!
宏,用于终止程序执行,并且抛出异常信息。当程序内部发生错误时,会自动调用penic!
,也可以手动调用。
fn main() {
// 编码错误,数组越界,会抛出异常,并终止执行
let v = vec![1, 2, 3];
v[99];
// 效果和调用penic!宏是一样的
panic!("error : 手动触发");
}
10.2 Result
类型异常捕获
对于某些调用,可能产生两种结果:成功、失败。
例如读取文件,文件打开时,代表成功的结果;文件不存在或权限不够,导致打开失败,代表失败的结果。
在Rust
中,使用Result<T,E>
来表示一个结果。该类型是一个枚举值:
enum Result<T, E> {
Ok(T),
Err(E),
}
对于返回Result
类型的函数来说,要么返回Result::Ok
,要么返回Result::Err
。例如一个读取文件的操作:
use std::fs::File;
fn main() {
let r = File::open("index.js");
let re =
match r {
Ok(file) => file,
Err(e) => panic!("{:?}", e),
};
}
这个例子中的错误对象,是结构体io::Error
的实例,它有很多方法,例如kind()
来获取错误信息的枚举值等;
expect和unwrap
为了简化处理Result
的情况,可以使用expect
和unwrap
两个函数,来对一个Result
类型的返回值做处理:
use std::fs::File;
fn main() {
let r = File::open("index.js");
// expect 是对 match + Result 的简化处理
// 如果值是 Result::Ok , expect直接返回这个Ok
// 如果值是 Result::Err , expect 则根据传入的&str,直接终止程序并打印
let r = r.expect("文件没有哇");
// let r = r.unwrap(); // unwrap 和 expect 功能差不多,就是直接返回Err
}
10.3 抛出异常
可以封装一个返回Result
类型的函数:
use std::fs::File;
use std::io;
fn main() {
let f = read_file("index.html").expect("没文件哇");
}
// 封装的函数,返回Result类型,可以在声明函数时,限定好泛型
// 返回值必须是下列两种类型:
// Result::Ok(T)
// Result::Err(E)
fn read_file(path: &str) -> Result<T, E> {
let r = File::open(path);
match r {
Ok(file) => Ok(file),
Err(e) => Err(e),
}
}
如果在函数中,使用到了多个返回Result
的函数,则函数体将会变得巨大,且难以维护。
Rust
官方提供了?
运算符,来简化Result
类型的返回:
如果调用的结果是Result::Ok , 则将结果返回;
如果调用的结果是Result::Err , 则将Err作为结果,然后作为整个函数的返回值,就像调用了return一样
use std::fs::File;
use std::io;
fn main() {
let f = read_file("index.html").expect("没文件哇");
}
fn read_file(path: &str) -> Result<T, E> {
// let r = File::open(path);
// match r {
// Ok(file) => Ok(file),
// Err(e) => Err(e),
// }
// 使用 ? 进行简化
// 如果open返回的是Err,则将其作为整个函数体的返回值
let r = File::open(path)?;
}
但是,?
只能用于返回Result
的函数.如果在main
函数,或者其他不返回Result
的函数中,使用?
是会报错的.
十一、泛型和trait
11.1 泛型
泛型,就是某个具体类型的替代.在rust中,用到了一种叫单态化
的技术:
通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程
函数中使用泛型
函数中使用泛型,能增肌抽象能力,减少重复函数, 在编译时,编译器根据调用的位置,会自动将泛型函数拆解,生成固定类型调用的函数。
fn main() {
console(100);
console("hello");
}
fn console<T: std::fmt::Debug>(logs: T) {
println!("{:?}", logs);
}
结构体中使用泛型
同样可以在结构体中,使用泛型参数,可以包含一个到多个泛型。 用泛型,来约束结构体中某几个字段的类型是否相同:
fn main() {
let btn = Button { width: 100, height: 100, text: "hello".to_string() };
println!("{}", btn.width);
let btn2 = Button { width: "hello", height: "world", text: 100 };
println!("{}", btn2.width);
// 两次实例化Button 结构,第一次width height 是数值,第二次width height 是字符串
}
struct Button<T, E> {
width: T,
height: T,
text: E,
}
在枚举类型中使用泛型
标准库中的Option
和Result
都是使用泛型来定义的枚举类型。
以Result
类型为例:
enum Result<T, E> {
Ok(T),
Err(E),
}
通过不同的方法,返回的Result
中的T和E是不同的,比如如果是读取文件,Result::Ok(T)
中的T传入的则是文件对象; 如果是读取文件内容,Result::Ok(T)
中的T传入的就是文件内容。
方法中的泛型
结构体和枚举类型的方法中,同样可以使用泛型。
// 首先在结构体中,定义泛型,约束各个字段
struct Point<T, U> {
x: T,
y: U,
}
// 实现结构体方法时,必须在impl中声明泛型
// 具体的方法,可以再使用泛型,且方法中的泛型签名可以是不一样的
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
// 这里,相当于返回了,self 的x,和传入的pont 的y
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); // 5,c
}
11.2 trait:行为
定义公共的行为,这些行为可以被其他的类型实现。类似于其他语言中接口的概念。
使用关键字trait
来定义:
fn main() {
// 由于两种结构体,都实现了同一个trait
// 所以都可以调用 draw 方法
let btn = Button { width: 100, height: 100 };
btn.draw();
btn.show_info();
btn.handle_click();
let img = Image { source: "http://sss/".to_string() };
img.draw();
img.show_info();
}
// 首先定义一个trait
pub trait Component {
// 只定义函数签名,实现的时候需要填充函数体,类似于抽象接口
fn draw(&self);
// 也可以声明具体的函数
fn show_info(&self) -> String {
println!("这里是默认的实现");
let r = "ok".to_string();
r
}
}
struct Button {
width: u32,
height: u32,
}
impl Button {
fn handle_click(&self) {
println!("按钮被点击方法");
}
}
// 给结构体 Button 来实现 trait
impl Component for Button {
fn draw(&self) {
println!("按钮渲染{}", self.height);
}
}
struct Image {
source: String,
}
// 给结构体 Image 来实现 trait
impl Component for Image {
fn draw(&self) {
println!("img 渲染:{}", self.source);
}
}
trait作为函数参数的类型标注
我们可以指定函数的参数类型,要求参数必须是实现了某个trait的类型:
fn main() {
let btn = Button { width: 100, height: 100 };
render(&btn);
// 其余代码
}
fn render(e: &impl Component) {
println!("在render中,调用传入参数的draw 方法");
e.draw(); // 然后调用 e 的 draw 方法
}
// 其余代码
其实,给形参标注impl trait名
的方法,是一种使用泛型语法的语法糖,恢复成泛型的表达方式,应该是这样的:
fn render<T: Component>(e: &T) {
println!("在render中,调用传入参数的draw 方法");
e.draw(); // 然后调用 e 的 draw 方法
}
另外,如果要求传入参数,是需要实现多个trait的情况下,可以使用+
将多个trait链接:
fn render<T: Component + Other>(e: &T) {
println!("在render中,调用传入参数的draw 方法");
e.draw(); // 然后调用 e 的 draw 方法
}
使用where简化
如果一个函数,有多个参数,而这些参数,分别需要约束它们去实现不同的trait,就是这种格式:
fn render<T: Component + Other, E: Trait1 + Trait2>(t: &T, e: &E) {}
这种格式是很难读懂函数签名的,为了让函数签名更加简介,rust提供了where
语法:
fn fn_name<T, E>(t: &T, e: &E) -> String
where T: Component + Other,
E: Trait1 + Trait2
{
// 函数体代码
}
11.3 生命周期
使用{}
嵌套一层,就会形成一个作用域,所谓的声明周期
,就是引用保持有效的作用域。
从外部作用域,来引用一个内部作用的引用,是不允许的:
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = & x; // | |
} // -+ |
// |
println ! ("r: {}", r); // |
} // ---------+
这段代码,编译的时候就会报错,提示&x
存活的时间不够长。因为在打印之前,它就被释放掉了。
同样的道理,如果这时候有个函数,分别传入了r
和x
的引用,而且返回其中的一个引用值, 函数本身编译的时候,不能确认传入的两个参数,谁的声明周期更长,也不知道具体返回的是哪个引用值,例如:
fn main() {
let s1 = "hello";
let s2 = "world";
large(s1, s2);
}
fn large(x: &str, y: &str) -> &str {
println!("{} - {}", x, y);
x
}
这个函数,在编译的时候就会报错,意思是说,返回值是借用类型,但是不知道是从x或y中哪个值返回的。
泛型生命周期函数
给函数传入一个泛型,这个泛型表示声明周期。
然后形参也同样声明这个生命周期,再传入实参后,生命周期会自动合并, 如果返回的也是引用的话,就约束返回的引用,是生命周期较短的那个。
fn main() {
let s1 = "hello".to_string();
let s3;
{
let s2 = "world".to_string();
s3 = large(&s1, &s2);
}
println!("{}", s3);
}
// 泛型寿命周期参数
// 传入实参后,'a 代表的声明周期,自动重叠,所以'a 代表的是最小的生命周期
// 然后返回值 'a 代表的也是最小的声明周期
// 如果返回的引用的声明周期短,就会报错
// 对于不需要参与生命周期计算的参数,可以不使用生命周期形参
fn large<'a>(x: &'a str, y: &'a str, z: &str) -> &'a str {
// x和y,y的生命周期较短
println!("{} - {}", x, y);
x // 返回值的生命周期,和生命周期较短的那个实参保持一致
// 所以 这个函数的返回值的生命周期,和y对齐,也就是s2
// 但是赋值操作,是赋值给s3,而s3的生命周期比函数返回值大,所以会报错
}
结构体定义泛型生命周期
再结构体中,也可以使用泛型生命周期:
struct ImportantExcerpt<'a> {
part: &'a str,
}
如果一个结构体的字段,定义了泛型生命周期,那么结构体本身的生命周期,不能高于其属性的生命周期。
可以在结构体的方法中,同样引入泛型生命周期,实现泛型生命周期方法,其遵守的规则,和下面的生命周期省略的规则一致:
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
生命周期的省略
在某些情况下,一直标注生命周期泛型是不必要的,所以有三条硬编码的规则,用来简化生命周期的标注:
- 每一个是引用的参数,都已自己的生命周期,如
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
,就是有两个周期 - 如果只有一个输入生命周期,那么它就赋予给所有输出生命周期
- 如果方法有多个输入生命周期,且参数有
&self
或者&mut self
,则说明这个函数是一个方法(例如结构体方法),则 输出生命周期,就是&self
的生命周期。
// 简写例子
fn first_word(s: &str) -> &str {}
// 根据第一条定律,增加输入生命周期,函数实际会变为
fn first_word<'a>(s: &'a str) -> &str {}
// 再根据第二个定律,增加输出生命周期
fn first_word<'a>(s: &'a str) -> &'a str {}
根据上面的例子,我们预期写的函数签名是fn first_word<'a>(s: &'a str) -> &'a str {}
,
实际上根据规则进行简化后,就可以简写为fn first_word(s: &str) -> &str {}
.
静态生命周期
如果生命周期,是贯穿程序期间的,可以使用特殊的标记'static
来标记。