《The Rust Programming language》代码练习(part 2 进阶部分)

我与Rust的缘分起始于当我在编程论坛上闲逛时,无意间发现了这么一门现代型的系统安全的函数式系统编程语言,但是当时只是大致了解,并无深入学习,所以此次便将它细致性地学习了一遍。
学习内容为书籍《The Rust Programming language》的全部内容(已完成)、《Rust编程之道》的全部内容(未完成)和《The Rustonomicon》的部分内容(未完成)。

一. 内容概述

我将 《Rust 编程语言》 的学习内容分为基础学习(1至9章)与进阶学习(10至19章),这两个部分是对我学习内容的一个大概缩略。而后是一个根据书上最后一章(20章)进行的简单的 web server 程序构建,最后是对比 Rust 社区已有的actix web 框架的一个简单 example。
本文为《The Rust Programming language》后半部分概要,此部分学习练习代码已经发在了开源平台 GiteeGitHub 平台上.

查看第二部分请转至

3.进阶学习

3.1泛型与trait

3.1.1泛型

​ 使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型.可以使用泛型定义函数、结构体、枚举和方法.

函数定义泛型:

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

​ 结构体定义泛型:

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

​ 枚举体定义泛型:(以自带的Result枚举定义为例)

enum Result<T, E> {
    Ok(T),
    Err(E),
}

​ 方法中的泛型定义:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

3.1.2 trait

​ trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

定义与实现trait:

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 {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

//Trait Bound 语法
pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

// where Trait Bound
fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{
    10
}

//return 实现了trait的类型
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,
    }
}

trait事实是一种高级接口用法,其使用使得代码编写变得灵活且可读性高.

3.2生命周期

​ Rust 中的每一个引用都有其 生命周期lifetime),也就是引用保持有效的作用域。生命周期实际上是一种泛型,生命周期的概念从某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能.

​ Rust 编译器有一个 借用检查器borrow checker),它比较作用域来确保所有的借用都是有效的,即对生命周期进行检查.

​ 生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。

​ 生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头,其名称通常全是小写,类似于泛型其名称非常短.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

​ 函数签名表明对于某些生命周期 'a,函数会获取两个参数,他们都是与生命周期 'a 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 'a 存在的一样长的字符串 slice

结构体定义中的生命周期注解:

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 };
}

​ 上述代码中出现的字符串切片成员(借用),类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数.

函数或方法的参数的生命周期被称为 输入生命周期input lifetimes),而返回值的生命周期被称为 输出生命周期output lifetimes)。

编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn 定义,以及 impl 块。

第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:fn foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。

第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32

第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法, 那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。

方法定义中的生命周期注解:

​ 譬如以下的代码符合第三条声明周期省略规则:

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

静态生命周期:

'static,一种特殊的生命周期,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static 生命周期.

标注例子:

let s: &'static str = "I have a static lifetime.";

​ 同一函数中指定泛型类型参数、trait bounds 和生命周期:

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
    }
}

3.3测试

​ Rust 中有可编写的测试函数用来验证非测试代码是否按照期望的方式运行。测试函数体通常执行如下三种操作:

  1. 设置任何所需的数据或状态

  2. 运行需要测试的代码

  3. 断言其结果是所期望的

    如下是一个简单的测试实例:

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));
    }
}

​ 当运行多个测试时, Rust 默认使用线程来并行运行。

3.4闭包

​ Rust 的 闭包closures)是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。

​ 闭包的定义以一对竖线(|)开始,在竖线中指定闭包的参数;

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

​ 使用move来捕获环境值得所有权;

fn main() {
    let x = vec![1, 2, 3];

    let equal_to_x = move |z| z == x;//在这里捕获x

    println!("can't use x here: {:?}", x);//此处报错,因为环境中的x已经无效了

    let y = vec![1, 2, 3];

    assert!(equal_to_x(y));
}

3.5智能指针

实际上前面基础部分所说的引用(&)是一种Rust中最常见的指针,引用以 & 符号为标志并借用了他们所指向的值.

智能指针smart pointers)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针 拥有 他们指向的数据。

​ 智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特性在于其实现了 DerefDrop trait。Deref trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop trait 允许我们自定义当智能指针离开作用域时运行的代码。

Box :

​ 最简单直接的智能指针是 box,其类型是 Box<T>。 box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Box多用于如下场景:

  • 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候

BOX创建递归类型:

​ Rust 需要在编译时知道类型占用多少空间。一种无法在编译时知道大小的类型是 递归类型recursive type),其值的一部分可以是相同类型的另一个值。这种值的嵌套理论上可以无限的进行下去,所以 Rust 不知道递归类型需要多少空间。不过 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

实现一个简易智能指针:

use std::ops::Deref;

struct MyBox<T>(T);
impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0
    }
}

#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    use crate::List::{Cons, Nil};

    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));

    let x = 5;
    let y = MyBox::new(x);

    println!("{:?}", list);

    assert_eq!(5, x);
    assert_eq!(5, *y);

    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

Rc:

​ 为了启用多所有权,Rust 有一个叫做 Rc<T> 的类型。其名称为 引用计数reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

Rc<T> 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。

Rc<T> 只能用于单线程场景.

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

​ Rc::strong_count()显示引用计数.

​ 过不可变引用, Rc<T> 允许在程序的多个部分之间只读地共享数据。如果 Rc<T> 也允许多个可变引用,则会违反第四章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。

RefCell:

RefCell<T> 代表其数据的唯一的所有权.

​ 如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么用户也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,虽然会带来不便,但不会带来灾难。RefCell<T> 正是用于当确信代码遵守借用规则,而编译器不能理解和确定的时候。

​ RefCell也只能用于单线程场景.

如下为选择 Box<T>Rc<T>RefCell<T> 的理由:

  • Rc<T> 允许相同数据有多个所有者;Box<T>RefCell<T> 有单一所有者。
  • Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T>仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查。
  • 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。

​ 在不可变值内部改变值就是 内部可变性 模式,RefCell智能指针正是为了内部可变性.

3.6 线程并发

并发编程Concurrent programming),代表程序的不同部分相互独立的执行,而 并行编程parallel programming)代表程序不同部分于同时执行

​ 已执行程序的代码在一个 进程process)中运行,操作系统则负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。运行这些独立部分的功能被称为 线程threads)。

​ 将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。

  • 竞争状态(Race conditions),多个线程以不一致的顺序访问数据或资源

  • 死锁(Deadlocks),两个线程相互等待对方停止使用其所拥有的资源,这会阻止它们继续运行

  • 只会发生在特定情况且难以稳定重现和修复的 bug

​ 由编程语言调用操作系统 API 创建线程的模型有时被称为 1:1,一个 OS 线程对应一个语言线程。编程语言提供的线程被称为 绿色green)线程,使用绿色线程的语言会在不同数量的 OS 线程的上下文中执行它们。为此,绿色线程模式被称为 M:N 模型:M 个绿色线程对应 N 个 OS 线程.

Rust 标准库只提供了 1:1 线程模型实现,绿色线程可选择手动实现或在社区寻找.

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

消息传递:

​ 确保线程安全并发的一个方式是 消息传递message passing),线程或 actor 通过发送包含数据的消息来相互沟通。

​ Rust 中一个实现消息传递并发的主要工具是 通道(channel

​ 通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。代码中的一部分调用发送者的方法以及希望发送的数据,另一部分则检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为通道被 关闭了。

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}

共享并发:

互斥器mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 lock)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 保护其数据。

use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

这里为了安全并发,使用了原子引用计数指针Arc,包装一个 Mutex<T> ,使其能够实现在多线程之间共享所有权.

3.7Rust与面向对象

如果用经典且传统的方式来定义面向对象:

面向对象的程序是由对象组成的。一个 对象 包含数据和操作这些数据的过程。这些过程通常被称为 方法 或 操作。

​ 在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被称为对象,但是他们提供了与对象相同的功能.

​ 如果封装是一个语言被认为是面向对象语言所必要的方面的话,那么 Rust 满足这个要求。在代码中不同的部分使用 pub 与否可以封装其实现细节。

​ 如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承父结构体的成员和方法。

3.8 unsafe Rust

​ Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 不安全 Rust(unsafe Rust

​ unsafe Rust存在的主要原因是:底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。Rust 需要能够进行像直接与操作系统交互.

​ 可以通过 unsafe 关键字来切换到unsafe Rust,接着可以开启一个新的存放unsafe 代码的块。有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,

  • 解引用裸指针

  • 调用不安全的函数或方法

  • 访问或修改可变静态变量

  • 实现不安全 trait

  • 访问 union 的字段

    unsafe 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe 关键字只是提供了以上五个不会被编译器检查内存安全的功能.

解引用裸指针:

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

调用不安全的函数或方法:

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

​ 使用 extern 函数调用外部代码:

​ 此处调用c语言库函数abs:

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

​ 导出为其他语言所用(需要编译为动态库)

​ 以导出为c语言所用为例:

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

访问或修改可变静态变量:

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

实现不安全 trait:

unsafe trait Foo {
    fn dangerous();
}

unsafe impl Foo for i32 {
    fn dangerous(){}
}

访问联合体中的字段:

​ 联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。

union MyUnion { f1: u32, f2: f32 }

fn f(u: MyUnion) {
    unsafe {
        match u {
            MyUnion { f1: 10 } => { println!("ten"); }
            MyUnion { f2 } => { println!("{}", f2); }
        }
    }
}

3.9高级函数

函数指针:

​ Rust中一切皆有类型,函数的类型是fn,实际上fn被称为函数指针,即可以将函数参数指定为函数指针类型.

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {}", answer);
}

​ 函数数指针实现了所有三个闭包 trait(FnFnMutFnOnce),所以总是可以在调用期望闭包的函数时传递函数指针作为参数,一个只期望接受 fn 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。

trait对象返回闭包:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

​ 这里对闭包进行了智能指针包装,后面采用trait对象的形式进行返回.

3.10宏

宏指的是 Rust 中一系列的功能:声明宏,使用 macro_rules!,和三种 过程宏:

  • 自定义 #[derive] 宏在结构体和枚举上指定通过 derive 属性添加的代码
  • 类属性(Attribute-like)宏定义可用于任意项的自定义属性
  • 类函数宏看起来像函数不过作用于作为参数传递的 token。

​ 从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming),所有的宏以展开 的方式来生成比所手写出的更多的代码。

rust基础语法与高阶特性学习完成之后,我尝试着进行了web服务器的编写,由于时间有限,只能进行简单的开发与实现,并且构建了单线程web server和利用rust特性进行重构和改进为多线程web server:

使用 macro_rules! 来定义宏:

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

过程宏定义宏:

use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

类属性宏:

#[route(GET, "/")]
fn index() {
}

类函数宏:

let sql = sql!(SELECT * FROM posts WHERE id=1);

这些宏展开之后都会有大量的代码,提高了编程的简洁性与效率.

进阶部分学习结语

该部分虽然相比起基础部分较为困难,例如闭包、测试和并发,但是依然可以跟随书籍循序渐进地学习,当然,相当多的内容还是需要去看其他书参考解决的。

本作品采用《CC 协议》,转载必须注明作者和本文链接
文章作者:chen0adapter
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!