《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》前半部分概要,此部分学习练习代码已经发在了开源平台 Gitee 和 GitHub 平台上.
二. 基础学习
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布尔类型有两个可能的值:true
和 false
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的所有权规则为:
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
Rust将所有权转移的行为称为move移动,例如以下这段代码:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
String::from()函数返回的是一个堆内存上变量的指针,如果在c++中,则会形成浅拷贝导致数据竞争或多次释放形成悬垂指针,造成潜在的安全漏洞,并且如果实现深拷贝会造成性能的降低。而在Rust中则实现了所有权移动,即如下所示:
即变量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];
内存引用就如同这样:
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>:
T
和 E
是泛型类型参数,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
值是成员 Ok
,unwrap
会返回 Ok
中的值。如果 Result
是成员 Err
,unwrap
会为我们调用 panic!
use std::fs::File;
fn main() {
let f = File::open("test.txt").unwrap();
}
expect
与 unwrap
的使用方式一样:返回文件句柄或调用 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 协议》,转载必须注明作者和本文链接