《The Rust Programming language》代码练习(part 1 基础部分)

我与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 平台上.

二. 基础学习

2.1Rust变量

​ 可变性和不可变性:

​ Rust变量默认是不可改变的(immutable),而使用mut关关键字创建可变变量,例如以下程序:

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);//5
    let mut y = 6;
    y = 7
    println!("The value of y is: {}", y);//7
}

用如下方法声明常量:

const MAX_POINTS: u32 = 100_000;

​ 变量遮蔽:使用let关键字对变量进行遮蔽,即如下三个x实际上不是同一个变量

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("The value of x is: {}", x);
}

2.2数据类型

​ Rust是一门面向表达式的函数式编程语言,与我学过的其他两种函数式编程语言Lisp和Haskell相比,Rust更像是中和了Lisp的抽象和Haskell的类型系统。Rust的每一个语句都是表达式,而每一个表达式都有其返回值,每一个返回值皆有其类型,所以Rust可以说是一切都有类型。且Rust是静态类型语言,编译器就必须知道所有变量的类型。

​ 这里简单介绍Rust的一些基本原生的数据类型:

2.2.1 标量类型

​ 标量(scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型

整型:

​ 下表展示了Rust原生的整数类型:

长度(bit) 有符号 无符号
8 i8 u8
16 i16 u16
32 i32 u32
64 i64 u64
128 i128 u128
arch isize usize

​ 其中arch的有符号整数与无符号整数类型长度(bit)依赖于所运行程序的计算机的架构。

浮点数:

​ 下表展示了Rust原生的浮点数类型:

长度(bit) 类型
32 f32
64 f64

​ Rust原生浮点数采用IEEE-754标准表示,f32为单精度浮点数,f64为双精度浮点数。

​ 例:

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

数值运算:

​ Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余

布尔类型:

​ Rust布尔类型有两个可能的值:truefalse

fn main() {
    let t = true;

    let f: bool = false; // 显式指定类型注解
}

可以将布尔类型转为整数类型0和1,但是不能将0和1转为布尔类型。

字符类型:

​ Rust‘的原生字符类型为四个字节的Unicode标量值,意味着可以表示中文、小表情等字符。

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

2.2.2 复合类型

元组类型:

​ 元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小,使用包含在圆括号中的逗号分隔的值列表来创建一个元组

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

数组类型:

​ Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。数组是一整块分配在栈上的内存,可以使用索引来访问数组的元素。

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
    let out = a[5];//数组越界报错
}

2.3函数

Rust提供两种函数,一种是具名函数,一种是匿名函数,匿名函数又被称为闭包(高级特性).

fn关键字被用来指定具名函数,后跟函数名,参数列表和返回值(如果返回值省略则由编译器自动加上 单元返回值() ),最后则是函数体(实际上是一个块表达式),Rust 代码中的函数和变量名使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {}", x);
}

fn plus_one(x: i32) -> i32 {
    let y = x;
    x + y
}

可以看见函数后面块表达式的值是最后一个表达式的值,即x+y;而非语句的值(语句返回())

{
    let y = x;
    x + y
}

2.4注释

在 Rust 中,注释必须以两道斜杠开始,并持续到本行的结尾

// this is a comment

当然,对于程序,Rust有一套标准的文档注释,比如///和/***/

///this is a doc comment

/**
    this is a
    multiline 
    comment
*/

2.5控制流

if表达式:

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

用let将if表达式的值获取:

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    println!("The value of number is: {}", number);
}

loop循环表达式:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

这里当counter值为10时,跳出loop循环并将counter倍乘为20

while条件循环表达式:

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number = number - 1;
    }

    println!("LIFTOFF!!!");
}

条件为真时执行while循环,所以这里while仅循环了三次。

for循环:

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

这里是返回in后面a调用iter()函数生成的迭代器,所以会输出a里面的每一个内容。

2.5所有权机制

2.5.1所有权介绍

​ Rust 的核心功能(之一)是 所有权(ownership),所有权(系统)是 Rust 最为与众不同的特性,它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全。

​ 实际上,在进行堆区数据管理的时候,一些语言中具有垃圾回收机制(Java、python),在程序运行时不断地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存(c、c++、D)。

​ Rust的所有权规则为:

  1. Rust 中的每一个值都有一个被称为其 所有者owner)的变量。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

Rust将所有权转移的行为称为move移动,例如以下这段代码:

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

​ String::from()函数返回的是一个堆内存上变量的指针,如果在c++中,则会形成浅拷贝导致数据竞争或多次释放形成悬垂指针,造成潜在的安全漏洞,并且如果实现深拷贝会造成性能的降低。而在Rust中则实现了所有权移动,即如下所示:
ownership
即变量s1不再有效,不能再使用。

克隆:(深拷贝)

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

​ 此时则会深拷贝堆内存变量到另s2,这样二者都有效,但是这样造成了性能浪费

2.5.2借用

​ 将获取引用作为函数参数称为 借用(borrowing),即不获取所有权,而获取操作权:

不可变借用:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

可变借用(可变引用):

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

​ Rust限制了在特定作用域中的特定数据只能有一个可变引用。这个限制的好处是 Rust 可以在编译时就避免数据竞争.

字符串slice:

​ 字符串 slice(string slice)是 String 中一部分值的引用,实际上所有的切片类型都是引用.

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

内存引用就如同这样:
字符串slice内存引用

2.6结构体

2.6.1基础结构体

Rust提供三种结构体:

  • 具名结构体
  • 元组结构体
  • 单元结构体

具名结构体及其方法:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {//返回一个结构体实例,功能类似于构造函数
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

​ impl为实现块,从上面可以看出一个实例(面向对象语言中叫对象)的实现块可以有多个且可拆分.

元组结构体:

​ 元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

单元结构体:

struct Empty;
fn main() {
    let x = Empty;
    println!("{:?}",&x);//单元结构体地址
}

2.6.2 枚举体

​ 枚举体以enum关键字定义,后跟枚举体名称,枚举成员,枚举允许存在不同类型的成员.

enum Message {
    Quit,//无参枚举成员
    Move { x: i32, y: i32 },//匿名结构体
    Write(String),//单参枚举成员
    ChangeColor(i32, i32, i32),//参枚举成员
}

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

​ 实际上枚举体是一种特殊的结构体,两者都能用以创建新类型.

2.7模式匹配

​ Rust有两种基础的控制流运算符可以进行模式匹配.分别是match控制流运算符模式匹配和if let简洁匹配.

match控制流如例:

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),//通配符匹配剩余匹配情况
}

if let控制流如例:

enum UsState {
   Alabama,
   Alaska,
}

enum Coin {
   Penny,
   Nickel,
   Dime,
   Quarter(UsState),
}
let coin = Coin::Penny;

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

​ 即简单情况下即用if let进行匹配与失配两种情况处理.

2.8Rust项目管理

​ Rust具有完整的模块系统(the module system)用来管理代码的组织.

  • 包(Packages): Cargo 的一个功能,它允许构建、测试和分享 crate。
  • Crates :一个模块的树形结构,它形成了库或二进制项目。
  • 模块(Modules)和 use: 允许你控制作用域和路径的私有性。
  • 路径(path):一个命名例如结构体、函数或模块等项的方式

​ crate 是一个二进制项或者库。crate root 是一个源文件,Rust 编译器以它为起始点,并构成crate 的根模块

​ 包(package) 是提供一系列功能的一个或者多个 crate.Crago 是Rust的包管理系统,类似于Java的maven和node.js的npm,一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate,以及依赖的外部包.

​ 一个包中至多 只能 包含一个库 crate(library crate);包中可以包含任意多个二进制 crate(binary crate);包中至少包含一个 crate,无论是库的还是二进制的

​ 模块让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。模块还可以控制项的 私有性,即项是可以被外部代码使用的(public),还是作为一个内部实现的内容,不能被外部代码使用(private)。

​ 一个包中的众多模块构成了模块树,模块不仅对于组织代码很有用.还定义了 Rust 的 私有性边界:这条界线不允许外部代码了解、调用和依赖被封装的实现细节。

​ Rust 中默认所有项(函数、方法、结构体、枚举、模块和常量)都是私有的。父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文。

​ 可以在Rust项之前使用pub关键字使其变为公有.

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

​ 可以使用 super 开头来构建从父模块开始的相对路径.

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }

    fn cook_order() {}
}

​ 可以使用 use 关键字调用路径中的项(必须是公开项).use支持嵌套

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
    pub mod fleeting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::{hosting,fleeting};
//use crate::front_of_house::*;//全导入

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    fleeting::add_to_waitlist();
    fleeting::add_to_waitlist();
}

​ 可以使用pub use关键字重导出项.

2.9集合

集合是Rust标准库中的一系列已经被实现的数据结构.这里仅记录三个.

  • vector: 允许一个挨着一个地储存一系列数量可变的值
  • String:字符的集合,所以是字符串的常用类型.
  • map:hash map的Rust标准库实现,将特定的键值对通过哈希函数关联.

vector:

​ vector允许存储多个相邻且相同数据类型的值.

let v = vec![100, 32, 57];
for i in &v {
    println!("{}", i);
}
let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

String:

​ 一种大小可增加,内容可改变的字符集合(字符串)

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{}-{}-{}", s1, s2, s3);

​ String虽然是字符串集合,是一个Vec的封装,但并不能支持索引,因为u8字符的特殊性,操作索引可能会使得u8标量值改变或分离,分解为多个单字节字符.

map:

​ HashMap<K, V> 类型储存了一个键类型 K 对应一个值类型 V 的映射。它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中.

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);

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

​ 这段代码使用 entry 方法只在键没有对应一个值时插入,所以向scores这个hash map插入了”blue”和50这个键值对,输出{“Yellow”: 50, “Blue”: 10}

2.10错误处理

​ Rust 将错误组合成两个主要类别:可恢复错误recoverable)和 不可恢复错误unrecoverable)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。

​ 大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常,但是,有可恢复错误 Result<T, E> ,和不可恢复(遇到错误时停止程序执行)错误 panic!

panic!:

​ panic!会导致程序栈的展开(清理栈数据)或终止(不清理栈数据就直接退出程序)

​ 以下是不可恢复错误的一个示例:

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

    v[99];
}

​ 主动调用panic!:

fn main() {
    panic!("crash and burn");
}

​ console会输出相应错误内容

Result<T, E>:

TE 是泛型类型参数,T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型

如下:

use std::fs::File;

fn main() {
    let f = File::open("test.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error)
        },
    };
}

​ 可以使用unwrap进行简写,如果 Result 值是成员 Okunwrap 会返回 Ok 中的值。如果 Result 是成员 Errunwrap 会为我们调用 panic!

use std::fs::File;

fn main() {
    let f = File::open("test.txt").unwrap();
}

expectunwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。expect 用来调用 panic! 的错误信息将会作为参数传递给 expect ,而不像unwrap 那样使用默认的 panic! 信息.

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

​ 可以用?实现传播错误:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;

    Ok(())
}

​ 即?可用于返回Result

基础部分学习结语:

该部分内容比较简单,虽然讲的都是基础语法,但是其实细究细节部分也是很有说法的,比如所有权规则、模式匹配和借用检查器,这些都十分能体现Rust的设计思想。

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

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