rust学习(第三章)

fist

非常推荐你自己一行行敲入这些代码,边写边思考为什么这么写,然后在运行时体会执行和输出的过程如果遇到了问题,你也可以点击每个例子附带的代码链接,在 Rust playground 中运行。

第一个实用rust程序

通过 HTTP 请求 Rust 官网首页,然后把获得的 HTML 转换成 Markdown 保存起来。

  1. 首先,我们用 cargo new scrape_url 生成一个新项目。默认情况下,这条命令会生成一个可执行项目 scrape_url,入口在 src/main.rs。我们在 Cargo.toml 文件里,加入如下的依赖:
[dependencies]
reqwest = { version = "0.11", features = ["blocking"] }
html2md = "0.2"

Cargo.toml 是 Rust 项目的配置管理文件,它符合 toml 的语法。我们为这个项目添加了两个依赖:reqwest 和 html2md。reqwest 是一个 HTTP 客户端,它的使用方式和 Python 下的 request 类似;html2md 顾名思义,把 HTML 文本转换成 Markdown。

  1. 在 src/main.rs 里,我们为 main() 函数加入以下代码:
use std::fs;

fn main() {
  let url = "https://www.rust-lang.org/";
  let output = "rust.md";
  
  println!("Fetching url: {}", url);
  let body = reqwest::blocking::get(url).unwrap().text().unwrap();

  println!("Converting html to markdown...");
  let md = html2md::parse_html(&body);

  fs::write(output, md.as_bytes()).unwrap();
  println!("Converted markdown has been saved in {}.", output);
}
  1. 保存后,在命令行下,进入这个项目的目录,运行 cargo run,在一段略微漫长的编译后
    在这里插入图片描述

  2. 程序开始运行,在命令行下,你会看到如下的输出:

    并且,在当前目录下,一个 rust.md 文件被创建出来了。打开一看,其内容就是 Rust 官网主页的内容。
    在这里插入图片描述

rust特点

从这段并不长的代码中,我们可以感受到 Rust 的一些基本特点:

  1. Rust 使用名为 cargo 的工具来管理项目
    它类似 Node.js 的 npm、Golang 的 go,用来做依赖管理以及开发过程中的任务管理,比如编译、运行、测试、代码格式化等等。
  2. Rust 的整体语法偏 C/C++ 风格
    函数体用花括号 {} 包裹,表达式之间用分号 ; 分隔,访问结构体的成员函数或者变量使用点 . 运算符,而访问命名空间(namespace)或者对象的静态函数使用双冒号 :: 运算符。如果要简化对命名空间内部的函数或者数据类型的引用,可以使用 use 关键字,比如 use std::fs。此外,可执行体的入口函数是 main()。
  3. Rust 虽然是一门强类型语言,但编译器支持类型推导
    很多不习惯类型推导的开发者,觉得这会降低代码的可读性,因为可能需要根据上下文才知道当前变量是什么类型。不过没关系,如果你在编辑器中使用了 rust-analyzer 插件,变量的类型会自动提示出来:
    在这里插入图片描述
  4. Rust 支持宏编程
    很多基础的功能比如 println!() 都被封装成一个宏,便于开发者写出简洁的代码。
  5. 这里例子没有展现出来,但 Rust 还具备的其它特点有:
  • Rust 的变量默认是不可变的,如果要修改变量的值,需要显式地使用 mut 关键字。
  • 除了 let / static / const / fn 等少数语句外,Rust 绝大多数代码都是表达式(expression)。所以 if / while / for / loop 都会返回一个值,函数最后一个表达式就是函数的返回值,这和函数式编程语言一致。
  • Rust 支持面向接口编程和泛型编程。
  • Rust 有非常丰富的数据类型和强大的标准库。
  • Rust 有非常丰富的控制流程,包括模式匹配(pattern match)。

Rust 开发的基本内容

变量和函数

在编译器能够推导类型的情况下,变量类型一般可以省略,但常量(const)和静态变量(static)必须声明类型

定义变量的时候,根据需要,你可以添加 mut
关键字让变量具备可变性。默认变量不可变是一个很重要的特性,它符合最小权限原则(Principle of Least
Privilege),有助于我们写出健壮且正确的代码。当你使用 mut 却没有修改变量,Rust 编译期会友好地报警,提示你移除不必要的
mut。

在 Rust 下,函数是一等公民,可以作为参数或者返回值。

fn apply(value: i32, f: fn(i32) -> i32) -> i32 {
    f(value)
}

fn square(value: i32) -> i32 {
    value * value
}

fn cube(value: i32) -> i32 {
    value * value * value
}

fn main() {
    println!("apply square: {}", apply(2, square));
    println!("apply cube: {}", apply(2, cube));
}

这里 fn(i32) -> i32 是 apply 函数第二个参数的类型,它表明接受一个函数作为参数,这个传入的函数必须是:参数只有一个,且类型为 i32,返回值类型也是 i32。

Rust 函数参数的类型和返回值的类型都必须显式定义,如果没有返回值可以省略,返回 unit。函数内部如果提前返回,需要用 return 关键字,否则最后一个表达式就是其返回值。如果最后一个表达式后添加了; 分号,隐含其返回值为 unit。可以在 Rust playground 中运行如下例子:

fn pi() -> f64 {
  3.1415926
}

fn not_pi() {
  3.1415926;
}

fn main() {
  let is_pi = pi();
  let is_unit1 = not_pi();
  let is_unit2 = {
    pi();
  };
  
  println!("is_pi: {:?}, is_unit1: {:?}, is_unit2: {:?}", is_pi, is_unit1, is_unit2);
}

rust函数中,如果最后一个表达式没有以;结尾,那么最后一个表达式就是它的返回值。
如果以;结尾则返回unit
如果在函数中需要提前返回则使用return
在这里插入图片描述

数据结构

#[derive(Debug)]
enum Gender {
  Unspecified = 0,
  Female = 1,
  Male = 2,
}

#[derive(Debug, Copy, Clone)]
struct UserId(u64);

#[derive(Debug, Copy, Clone)]
struct TopicId(u64);

#[derive(Debug)]
struct User {
  id: UserId,
  name: String,
  gender: Gender,
}

#[derive(Debug)]
struct Topic {
  id: TopicId,
  name: String,
  owner: UserId,
}

// 定义聊天室中可能发生的事件
#[derive(Debug)]
enum Event {
  Join((UserId, TopicId)),
  Leave((UserId, TopicId)),
  Message((UserId, TopicId, String)),
}

fn main() {
    let alice = User { id: UserId(1), name: "Alice".into(), gender: Gender::Female };
    let bob = User { id: UserId(2), name: "Bob".into(), gender: Gender::Male };
    
    let topic = Topic { id: TopicId(1), name: "rust".into(), owner: UserId(1) };
    let event1 = Event::Join((alice.id, topic.id));
    let event2 = Event::Join((bob.id, topic.id));
    let event3 = Event::Message((alice.id, topic.id, "Hello world!".into()));
    
    println!("event1: {:?}, event2: {:?}, event3: {:?}", event1, event2, event3);
}

简单解释一下:

  1. Gender:一个枚举类型,在 Rust 下,使用 enum 可以定义类似 C 的枚举类型
  2. UserId/TopicId :struct 的特殊形式,称为元组结构体。它的域都是匿名的,可以用索引访问,适用于简单的结构体。
  3. User/Topic:标准的结构体,可以把任何类型组合在结构体里使用。
  4. Event:标准的标签联合体,它定义了三种事件:Join、Leave、Message。每种事件都有自己的数据结构。
    在定义数据结构的时候,我们一般会加入修饰,为数据结构引入一些额外的行为。
    在定义 UserId / TopicId 时我们还用到了 Copy / Clone 两个派生宏,Clone 让数据结构可以被复制,而 Copy 则让数据结构可以在参数传递的时候自动按字节拷贝。

总结一下Rust 定义变量、函数和数据结构:
在这里插入图片描述

控制流程

程序的基本控制流程分为以下几种:
顺序执行就是一行行代码往下执行。在执行的过程中,遇到函数,会发生函数调用。
函数调用是代码在执行过程中,调用另一个函数,跳入其上下文执行,直到返回。
Rust 的循环和大部分语言都一致,支持死循环 loop、条件循环 while,以及对迭代器的循环 for。循环可以通过 break 提前终止,或者 continue 来跳到下一轮循环。
满足某个条件时会跳转, Rust 支持分支跳转、模式匹配、错误跳转和异步跳转。

  • 分支跳转就是我们熟悉的 if/else;
  • Rust 的模式匹配可以通过匹配表达式或者值的某部分的内容,来进行分支跳转;
  • 在错误跳转中,当调用的函数返回错误时,Rust 会提前终止当前函数的执行,向上一层返回错误。
  • 在 Rust 的异步跳转中 ,当 async 函数执行 await 时,程序当前上下文可能被阻塞,执行流程会跳转到另一个异步任务执行,直至 await 不再阻塞。
    我们通过斐波那契数列,使用 if 和 loop / while / for 这几种循环,来实现程序的基本控制流程(代码):
fn fib_loop(n: u8) {
    let mut a = 1;
    let mut b = 1;
    let mut i = 2u8;
    
    loop {
        let c = a + b;
        a = b;
        b = c;
        i += 1;
        
        println!("next val is {}", b);
        
        if i >= n {
            break;
        }
    }
}

fn fib_while(n: u8) {
    let (mut a, mut b, mut i) = (1, 1, 2);
    
    while i < n {
        let c = a + b;
        a = b;
        b = c;
        i += 1;
        
        println!("next val is {}", b);
    }
}

fn fib_for(n: u8) {
    let (mut a, mut b) = (1, 1);
    
    for _i in 2..n {
        let c = a + b;
        a = b;
        b = c;
        println!("next val is {}", b);
    }
}

fn main() {
    let n = 10;
    fib_loop(n);
    fib_while(n);
    fib_for(n);
}

这里需要指出的是,Rust 的 for 循环可以用于任何实现了 IntoIterator trait 的数据结构。

在执行过程中,IntoIterator 会生成一个迭代器,for 循环不断从迭代器中取值,直到迭代器返回 None 为止。因而,for 循环实际上只是一个语法糖,编译器会将其展开使用 loop 循环对迭代器进行循环访问,直至返回 None。

在 fib_for 函数中,我们还看到 2…n 这样的语法,想必 Python 开发者一眼就能明白这是 Range 操作,2…n 包含 2<= x < n 的所有值。和 Python 一样,在 Rust 中,你也可以省略 Range 的下标或者上标,比如:

let arr = [1, 2, 3];
assert_eq!(arr[..], [1, 2, 3]);
assert_eq!(arr[0..=1], [1, 2]);

和 Python 不同的是,Range 不支持负数,所以你不能使用 arr[1…-1] 这样的代码。这是因为,Range 的下标上标都是 usize 类型,不能为负数。

下表是 Rust 主要控制流程的一个总结:
在这里插入图片描述

模式匹配

Rust 的模式匹配吸取了函数式编程语言的优点,强大优雅且效率很高。它可以用于 struct / enum 中匹配部分或者全部内容,比如上文中我们设计的数据结构 Event,可以这样匹配

fn process_event(event: &Event) {
    match event {
        Event::Join((uid, _tid)) => println!("user {:?} joined", uid),
        Event::Leave((uid, tid)) => println!("user {:?} left {:?}", uid, tid),
        Event::Message((_, _, msg)) => println!("broadcast: {}", msg),
    }
}

从代码中我们可以看到,可以直接对 enum 内层的数据进行匹配并赋值,这比很多只支持简单模式匹配的语言,例如 JavaScript 、Python ,可以省出好几行代码。

除了使用 match 关键字做模式匹配外,我们还可以用 if let / while let 做简单的匹配,如果上面的代码我们只关心 Event::Message,可以这么写

fn process_message(event: &Event) {
    if let Event::Message((_, _, msg)) = event {
        println!("broadcast: {}", msg);   
    }
}

Rust 的模式匹配是一个很重要的语言特性,被广泛应用在状态机处理、消息处理和错误处理中,如果你之前使用的语言是 C / Java / Python / JavaScript ,没有强大的模式匹配支持,要好好练习这一块。

错误处理

Rust 没有沿用 C++/Java 等诸多前辈使用的异常处理方式,而是借鉴 Haskell,把错误封装在 Result 类型中,同时提供了 ? 操作符来传播错误,方便开发。Result 类型是一个泛型数据结构,T 代表成功执行返回的结果类型,E 代表错误类型。
今天开始的 scrape_url 项目,其实里面很多调用已经使用了 Result 类型,这里我再展示一下代码,不过我们使用了 unwrap() 方法,只关心成功返回的结果,如果出错,整个程序会终止。

use std::fs;
fn main() {
  let url = "https://www.rust-lang.org/";
  let output = "rust.md";
  
  println!("Fetching url: {}", url);
  let body = reqwest::blocking::get(url).unwrap().text().unwrap();

  println!("Converting html to markdown...");
  let md = html2md::parse_html(&body);

  fs::write(output, md.as_bytes()).unwrap();
  println!("Converted markdown has been saved in {}.", output);
}

如果想让错误传播,可以把所有的 unwrap() 换成 ? 操作符,并让 main() 函数返回一个 Result,如下所示:

use std::fs;
// main 函数现在返回一个 Result
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://www.rust-lang.org/";
    let output = "rust.md";

    println!("Fetching url: {}", url);
    let body = reqwest::blocking::get(url)?.text()?;

    println!("Converting html to markdown...");
    let md = html2md::parse_html(&body);

    fs::write(output, md.as_bytes())?;
    println!("Converted markdown has been saved in {}.", output);

    Ok(())
}

Rust 项目的组织

当 Rust 代码规模越来越大时,我们就无法用单一文件承载代码了,需要多个文件甚至多个目录协同工作,这时我们可以用 mod 来组织代码。

具体做法是:在项目的入口文件 lib.rs / main.rs 里,用 mod 来声明要加载的其它代码文件。如果模块内容比较多,可以放在一个目录下,在该目录下放一个 mod.rs 引入该模块的其它文件。这个文件,和 Python 的 init.py 有异曲同工之妙。这样处理之后,就可以用 mod + 目录名引入这个模块了,如下图所示:
在这里插入图片描述
在 Rust 里,一个项目也被称为一个 crate。crate 可以是可执行项目,也可以是一个库,我们可以用 cargo new – lib 来创建一个库。当 crate 里的代码改变时,这个 crate 需要被重新编译。

在一个 crate 下,除了项目的源代码,单元测试和集成测试的代码也会放在 crate 里。

Rust 的单元测试一般放在和被测代码相同的文件中,使用条件编译 #[cfg(test)] 来确保测试代码只在测试环境下编译。以下是一个单元测试的例子:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

集成测试一般放在 tests 目录下,和 src 平行。和单元测试不同,集成测试只能测试 crate 下的公开接口,编译时编译成单独的可执行文件。

在 crate 下,如果要运行测试用例,可以使用 cargo test。

当代码规模继续增长,把所有代码放在一个 crate 里就不是一个好主意了,因为任何代码的修改都会导致这个 crate 重新编译,这样效率不高。我们可以使用 workspace。

一个 workspace 可以包含一到多个 crates,当代码发生改变时,只有涉及的 crates 才需要重新编译。当我们要构建一个 workspace 时,需要先在某个目录下生成一个如图所示的 Cargo.toml,包含 workspace 里所有的 crates,然后可以 cargo new 生成对应的 crates:
在这里插入图片描述

小结

在这里插入图片描述

  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值