rust极速入门(二)

九、集合

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的情况,可以使用expectunwrap两个函数,来对一个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,
}

在枚举类型中使用泛型

标准库中的OptionResult都是使用泛型来定义的枚举类型。

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存活的时间不够长。因为在打印之前,它就被释放掉了。

同样的道理,如果这时候有个函数,分别传入了rx的引用,而且返回其中的一个引用值, 函数本身编译的时候,不能确认传入的两个参数,谁的声明周期更长,也不知道具体返回的是哪个引用值,例如:

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来标记。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值