了解到 rust 和 WebAssembly 的结合使用,可以构建前端应用,而且性能也比较好。初步学习使用
rust 是预编译静态类型语言。
安装 rust
官网下载 rust-CN , 大致了解下为什么选择:高性能、可靠性、生产力。
打开控制台啊,执行安装 (mac 系统,windwos 或其他系统查看官网)
&> curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安装成功时,会打印:
或则会通过查看版本检查是否安装更新:
$> rustc --version
有打印就成功了。
rust 通过 rustup 来管理。更新 rust
rustup update
更新 rustrustup self uninstall
写在 rust 和 rustup 管理器。
rust 一些常用的包依赖 c 语言,需要安装 C 编译器
如果你不是一个 c/c++开发者,则一些常用的 rust 在使用时,会报错。根据不同的系统需要安装 c 编译器。
- mac 系统安装
xcode-select
$> xcode-select --install
- windwos 系统则需要安装
visual studio
。
visualstudio
不安装时,报错如图:
起步项目 hello world
rust 的主程序代码文件都以.rs
结尾
$> mkdri rust-web
$> cd rust-web
$> vi main.rs
编辑main.rs
文件,写入以下内容
fn main() {
println!("Hello, world!");
}
编译文件并执行
$> rustc main.rs
$> ./main
可以看到控制打印输出
main
是主函数入口,rust 特殊的函数,最先执行。
println!
表示调用的是宏(macro) ,它不是普通的函数。所以并不总是遵循与函数相同的规则
认识 cargo
Cargo
是 Rust 的构建系统和包管理器,可以帮助我们构建代码、下载依赖库并编译这些库
通过查看版本检查是否安装:
$> cargo --version
cargo
管理项目,构建工具以及包管理器
-
cargo build
构建项目可以通过
cargo build --release
构建生产包,增加了编译时间,但是的代码可以更快的运行。 -
cargo run
运行项目 -
cargo test
测试项目 -
cargo doc
为项目构建文档 -
cargo publish
将项目发布到 crates.io -
cargo check
快速检查代码确保其可以编译
打印成功则安装成功.
创建一个项目,cargo new rust-web
; 因为我已经创建了项目目录,所以使用cargo init
进行初始化
初始化完目录如下:
Cargo.toml
为项目的清单文件。包含了元数据信息以及项目依赖的库,在 rust 中,所有的依赖包称为crates
。
[package]
name = "rust-web"
version = "0.1.0"
edition = "2021"
[dependencies]
src
目录则是项目功能文件目录,可以看到文件后缀名是.rs
fn main() {
println!("Hello, world!");
}
启动执行cargo run
, 如果启动成功,就会打印出来
cargo build
构建编译,可以在target
目录下看到,
通过执行./target/debug/rust-web
,可以看到和上面输出同样的内容;
rust 基础语法
学习一门新语言,首先要掌握它的语法,怎么去写、表达。
变量、基本类型、函数、注释和控制流
变量与可变性
let
定义一个变量,更改后打印输出。
fn main() {
let age = 24;
print!("{age}");
age = 34;
print!("{age}");
}
执行cargo check
,可以看到打印输出,不允许更改。
如果需要变更,则需要添加mut
标识变量可变。但不可以更改变量的类型
fn main() {
let mut age = 24;
print!("{age}");
age = 34;
print!("{age}");
}
const
声明一个常量
常量声明时,需要明确标注出数据类型。
fn main() {
const Age: u32 = 200;
}
变量隐藏
因为不可变性,我们不能对一个变量重复复制,但可以通过对变量的重复声明,来实现变量的重复声明。(变量仍然是不可更改的)
fn main() {
let age = 24;
print!("{age}");
let age = 34;
print!("{age}");
}
在局部作用域结束后,变量仍为初始声明的值。
fn main() {
let age = 24;
print!("{age}");
{
let age = 34;
print!("{age}");
}
print!("{age}");
}
输出的值为24 34 24
数据类型
了解了数据类型,在声明变量时标明变量的类型。rust 是静态语言,编译时就需要指定所有变量的类型。
数据类型分为两个大类型:标量(scalar)、复合(compound)。
标量类型
表示一个单独的值,有四种基本的类型:整型、浮点型、布尔类型、字符类型。
整型
整型是一个没有小数的数字。包括有符号位、无符号位。
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 | u8 |
16bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
有符号位的可以存储 − ( 2 n − 1 ) -(2^{n-1}) −(2n−1)到 2 n − 1 2^{n-1} 2n−1的数字。
isize和usize
则依赖运行程序的计算机架构,64 位架构则就是 64 位的,32 位架构就是 32 位的。
fn main() {
let num: i16 = -1000;
print!("{num}");
}
除了十进制数字作为变量值,也可以十六进制、八进制、二进制、Byte(单字节字符)来表示。
数字字面值 | 示例 |
---|---|
十进制 | 100,250 |
十六进制 | 0xff |
八进制 | 0o67 |
二进制 | 0b111000 |
Byte 单子节字符(仅限于 u8) | b’A’ |
对于整型变量值还可以通过_
做分隔符,以方便读数字,比如23_10
,也就是十进制的2310
.
fn main() {
let num: i16 = -1_000; // 1000
let age: i8 = 0b1100; // 12
}
初学者可以使用默认的类型,即不需要书写声明类型,rust 会有一个默认的类型。数字默认是i32
当我们指定了类型长度后,在编程中可能会出现超出,超过我们指定的存储大小。
整型溢出
浮点型
浮点型包括f32\f64
.所有的浮点数都是有符号的。浮点数采用 IEEE-754 标准表示
f32
是单精度浮点数f64
是双精度浮点数
rust 默认浮点数类型位f64
加、减、乘、除、取余操作
整数除法会向下舍入到最接近的整数
fn main() {
let value = 9 / 7; // 1
}
布尔类型bool
: true、false
字符类型 - char
fn main() {
let char = 'a'
}
用单引号声明 char 字符值,使用双引号声明字符串值。
复合类型
复合类型是将多个值组合成一个类型,包括元组、数组。
元组 tuple
元组长度固定,声明后就不会被改变。可以由不同类型的值组成。
fn main() {
let tup:(i8,u16,i64) = (54,500,1000)
// 通过结构取值
let (a,b,c) = tup
// 通过下表直接读取
let a = tup.0;
let b = tup.1;
}
不带任何值的元组称为单元元组。
数组
数组中的每个数据类型必须相同。数组的长度是固定的。
fn main() {
let arr=[34,45,5,67]
}
数组类型标记为arr:[i32; 4]
表示数据类型为i32
,共有 4 个元素。
通过数组的下表访问数组元素arr[0]\arr[1]
函数
通过使用fn
来定义函数。函数名命名方式建议使用_
连接。
fn main(){
// 函数体
}
只要在相同作用域内声明的函数,不管声明在前或在后,都可以调用。
fn main(){
println!("hello world");
}
// 函数声明在调用之后
fn user_info(){
//
println!("user");
}
函数必须声明每一个接受的参数,并且需要指定其数据类型。
// 函数声明在调用之后
fn user_info(age: i32){
//
println!("user");
}
语句和表达式
rust是一门基于表达式的语言。其它语言没有,
语句是执行一些操作但不返回值的指令。表达式计算并产生一个值。
不能将一个变量赋值给另一个声明的变量
fn main(){
let num:i32;
// 这样写是错误的,如果没有指定数据类型,rust会默认指定age数据类型为单元元组
let age=num=32;
// 指定age的数据类型,则age不能正常赋值,报错
let age:i32=num=32;
}
let age=num=32
rust会默认指定age为单元元组()
不带任何值。num赋值为32;
通过大括号{}
可声明一个作用域快:
fn main(){
let a = {
let b = 45;
b+32
// b+32;
}
println!("{a}");
}
可以看到b+32
后面没有加;
,就是一个表达式,会有值返回;加了;
就是一个语句了。
语句不会返回值。a
的数据类型就是单元元组。
函数返回值
函数的最后一个表达式就是函数的返回值。也可以通过return
关键字返回指定值。
函数的返回值必须指定其数据类型
fn user_info(age: i32)->i32{
//
age*2
}
接受一个参数值,返回其乘积。如果计算超出时类型长度时,需要转换成长度更大的数据类型。
// TODO:
fn user_info(age: i8)->i32{
//
age*200
}
控制语句
if
条件判断语句,必须是显示bool
类型作为判断条件。rust不会隐世转换数据类型。
fn user_info(age: u8){
//
if age > 50 {
println!("中年");
} else if age > 30 {
println!("壮年");
} else if age > 18 {
println!("青年");
}
}
在声明语句中通过条件语句,绑定不同的结果值。所有分支中的数据类型必须是相同的
fn main(){
let bool=false;
let age= if bool {20} else {34};
}
循环语句
包括三种:loop / while / for
loop
循环执行,直到终止,也可程序终止通过break
示例通过循环执行来达到想要的值。break
终止循环,通过后面跟表达式来返回表达式的值。
fn main(){
let mut num = 50;
let age = loop{
num-=5;
if num<40 {
break num+1;
}
};
}
当存在多层循环时,break只能循环结束它自己这一层的循环。可以通过增加循环标签,break <label>
可以指定循环结束。
fn main(){
let mut count = 0;
'out_in: loop {
println!("out");
let mut num = 10;
loop {
println!("{num}");
if num < 7 {
break;
}
if count == 4 {
break 'out_in;
}
num -= 1;
}
count += 1;
}
}
'out_in
标识循环标签。注意是左单引号。
while
循环
fn main(){
let mut count = 0;
while count<5{
println!("{count}");
count += 1;
}
}
if
循环,while更多方便用于条件语句循环执行;if则更适合遍历结构化数据。
fn main(){
let arr = [34,56,23,34];
for val in arr{
println!("{val}");
}
}
所有权
这是rust独有的特性。rust无需垃圾回收即可保障内存安全。
- rust中的每一个值都有一个所有者。
- 值在任一时刻有且只有一个所有者
- 当所有者(变量)离开作用域,这个值将被丢弃。
rust管理内存的方式在变量离开作用域后就会被自动释放。rust在作用于结束后,自动调用内部一个特殊的函数drop
。
简单的已知数据长度类型的值存储时存储在栈
中。比如:所有整数、布尔、所有浮点数、字符类型char、元组(其中的每个元素都是已知大小的)
let a:u8 = 32;
let b = a;
这里声明了变量a
,并将a的值赋值给了b。b拷贝了a的值存入栈中。也就是栈存储有两个值32.
而对于一些不可知大小的变量存储,是存放在堆
中的。
String
类型,定义的数据值分配到堆中管理,
let a = String::from("hboot");
let b = a;
此时声明的变量b拷贝了变量a存储在栈中的指针、长度和容量。指针指向仍是堆中同一位置的数据。
rust为了方便处理内存释放,防止两次内存释放bug产生,变量a被赋值给变量b后,就失效了。不在是一个有效的变量。
这种行为可以称为所有权转移。转移的变量就不存在了,不可访问。
那如果想要重复两个变量数据变量,可以通过克隆clone
let a = String::from("hboot");
let b = a。close();
这样我们存储了双份的数据在内存中。
在函数中,所有权转移
在传递参数时,参数会将所有权转移,使得定义的变量失效
let a = String::from("hboot");
print_info(a); // a的多有权转移到函数print_info中
//后续访问a则访问不到。
let b = a.clone(); // 编译会报错,无法执行
通常日常中,这样会导致一些麻烦,声明的变量值后续还要用。可以通过调用函数返回,重新拿到所有权继续使用该变量
fn main(){
let a = String::from("hboot");
let b = print_info(a); // a 所有权转移到函数print_info中
// 通过函数返回值所有权又转移到 变量b
}
fn print_info(str: String) -> String {
str
}
为了阻止所有权转来转去,可以通过函数返回元组,来表示只是使用值,不需要所有权
fn print_info(str: String) -> (String) {
(str)
}
每次调用都要注意返回参数,很麻烦。可以通过引用来不转移所有权。
引用
通过使用&
传参、接参表名只是值引用。而不转移所有权
fn main(){
let a = String::from("hboot");
let b: usize = print_info(&a);
// 此处变量a仍然可用
println!("{}-{}", a, b);
}
fn print_info(str: &String) -> usize {
str.len()
}
因为是引用值,print_info
函数执行结束,变量str
不会有内存释放的操作
所以引用的变量是不允许更改的。
通过解引用
*
进行引用相反的操作。
如果需要更改引用变量,则需要通过mut
fn main(){
let mut a = String::from("hboot");
let b: usize = print_info(&mut a);
// 此处变量a仍然可用
println!("{}-{}", a, b);
}
fn print_info(str: &mut String) -> usize {
str.push_str(",hello");
str.len()
}
首先声明变量可变,传参创建可变引用&mut
,还有函数签名str:&mut String
.
可变引用需要注意的是,同时不能存在多个可变引用。会出现数据竞争导致未定义。也不能在有不可变引用的同时有可变引用。
let mut a = String::from("hboot");
// 同时多个可变引用是不可行的。
// 必须等上一个引用结束
let b = &mut a;
let c = &mut a; // 这里就会报错
// 这里有用到变量b
print!("{}-{}",b,c);
当一个函数体执行完毕后,所有使用到的内存都会被自动销毁。如果我们返回了其中变量的引用,则会报错,称为悬垂引用
fn print_info() -> &mut String {
let str = String::from("hboot");
// 这是错误的,函数执行完毕,必须交出所有权
&str
}
转移所有权,不能使用引用作为返回。
slice 类型
slice
截取变量数据的一部分作为引用变量。所以它是没有所有权的。
let str = String::from("hboot");
let substr = &str[0,3]; // hbo
可以看到通过[start..end]
来截取字符串的一部分。如果是start
是0 可以省略[..3]
;如果end
是包含最后一个字节,则可以省略
let str = String::from("hboot");
let len = str.len();
let substr = &str[0..len]; // 同等
let substr = &str[..];
可以看到使用slice的引用变量rust默认类型为&str
,这是一个不可变引用。
现在可以更改函数print_info传参,使得它更为通用
fn main(){
let a = String::from("hboot");
// 整个字符串引用
print_info(&a[..]);
// 截取引用
print_info(&a[1..3]);
let b = "nice rust"
// 也可以传递字符串字面值
print_info(b);
}
fn print_info(str: &str) {
println!(",hello");
}
除了字符串,还有数组可被截取引用。
let a: [i32; 4] = [3,4,5,12]
let b: &[i32] = &a[1..4]
结构体struct
通过struct
来定义一个结构体,它是一个不同数据类型的集合。键定义名称,定义键值类型。
struct User {
name:String,
age:i32,
email:String,
id:u64
}
结构体可以定义变量,这个数据则必须包含结构体中包含的所有字段。
let user = User {
name: String::from("hboot"),
age: 32,
email: String::from("bobolity@163.com"),
id: 3729193749,
};
如果需要修改,则需要定义为可变变量let mut user
。不允许定义某一个字段可变。
结构体更新语法可以从其他实例创建一个新实例。
// 重新创建一个实例,不需要再挨个字段赋值
let other_user = User {
name: String::from("admin"),
..user
};
..
语法指定剩余未显示设置值的字段与给定实例相同的值。必须放在后面,以便其获取给定实例中它没有指定的字段值。
这里同样适用所有权的转移,我们在实例中重新设置了name
,那么原始对象user.name
仍然是可访问的。
对于字段user.email
则是不可访问的。它已经转移到other_user.email
了。
不能直接指定结构体的数据类型为
$str
,在生命周期一节解决这个问题 。
元组结构体
创建和元组一样的没有键的结构体。
struct Color(i32,i32,i32);
只指定了数据类型,在一些场景下是有用的。
let color = Color(124,233,222);
类单元结构体
没有任何字段的结构体。类似于单元元组()
struct HelloPass;
需要在某个类型上实现trait但不需要在类型中存储的时候发挥作用。
方法语法
可以在结构体中定义方法。来实现和该结构体相关的逻辑。通过impl
关键字定义:
struct User {
name: String,
age: i32,
email: String,
id: u64,
}
impl User {
fn getAgeDesc(&self) -> &str {
if self.age > 50 {
return "中年";
} else if self.age > 30 {
return "壮年";
} else if self.age > 18 {
return "青年";
}
return "少年";
}
}
方法也可以接受参数和返回值。方法中的第一个参数self
指向结构体实例本身。可以获取结构体中定义的字段数据。
示例中&self
引用,不需要所有权,如果需要控制实例,更改实例数据,则需要更改为&mut self
定义方法时,也可以定义和属性同名的方法。在调用时,方法需要加()
;而属性不需要。
也可以定义self
不作为参数的关联函数,这样它就不会作用于结构体实例。这一类函数常用来返回一个结构体新实例的构造函数。
我们通过元组方式传递结构体需要的四个属性值来创建一个新实例。
impl User {
fn admin(user: (String, i32, String, u64)) -> Self {
Self {
name: user.0,
age: user.1,
email: user.2,
id: user.3,
}
}
}
如上,定义了一个关联函数admin
,接受一个元组参数,并用其中的四个值来赋值给结构体的几个字段。
let user_one = User::admin((
String::from("test"),
45,
String::from("123@qq.com"),
452411232,
));
dbg!(&user_one);
这样的关联函数需要通过::
语法来调用。实例一直在用String::from()
是同样的逻辑。
这样做的好处在于可以免去初始化赋值的麻烦。当然这也需要你知道每个传参定义的是什么数据类型。
枚举、模式匹配
枚举就是通过列举所有可能的值来定义一个类型。是将字段和数据值据合在一起。
通过使用enum
来定义枚举值。
enum Gender {
Boy,
Girl,
}
枚举值通常使用驼峰书写。通过::
语法实例化枚举值
let boy = Gender::Boy;
可以将枚举作为类型定义在结构体中。这样字段gender
的值只能是枚举中定义的。
struct User {
name: String,
age: i32,
email: String,
id: u64,
gender: Gender
}
以上仅仅表达了性别,如果还想表达更多关联的值,除了在结构体定义其他字段来存储,也可以在枚举值绑定数据值表达。
enum Gender {
Boy(String,i32),
Girl(String,i32),
}
附加两个数据值,一个String
,一个i32
let boy = Gender::Boy(String::from("男孩"), 1);
也可以将结构体作为枚举数据类型。在枚举中也可以定义譬如结构体的方法。
impl Gender {
fn getHobby(&self){
// 这里可以返回男孩、女孩不同的爱好选项
}
}
fn main(){
let boy = Gender::Boy(String::from("男孩"), 1);
&boy.getHobby();
}
Option
枚举被广泛运用于处理一个值要么有值要么没值。
enum Option<T>{
None,
Some(T)
}
fn main(){
let num1 = 32;
// 枚举定义的值
let num2: Option<i32> = Some(32);
}
他们是不同的,num1
类型是i32一个明确有效的值;而num2
类型为Option<i32>
不能确保有值。
match
控制流结构
通过match
语法可以通过对枚举值的匹配不同执行不同的业务逻辑
enum Gender {
Boy,
Girl,
}
// 定义一个函数接受gender枚举值作为参数
// 通过match匹配执行不同的逻辑
fn get_gender_code(gender: Gender) -> u8 {
match gender {
Gender::Boy => {
print!("男孩");
1
}
Gender::Girl => {
print!("女孩");
2
}
}
}
fn main(){
let boy = Gender::Boy;
dbg!(getHobby(boy));
}
如果枚举绑定了数据,也可以通过匹配模式获取到枚举数据。
// Gender采用之前定义过的有数据绑定的模式
fn get_gender_code(gender: Gender) -> i32 {
match gender {
Gender::Boy(label, code) => {
print!("{}", label);
code
}
Gender::Girl(label, code) => {
print!("{}", label);
code
}
}
}
fn main(){
let boy = Gender::Boy(String::from("男孩"), 1);
dbg!(getHobby(boy));
}
还有Option
也可以被匹配。通过匹配来处理有值的情况下。
fn plus_one(val: Option<i32>) -> Option<i32> {
match val {
None => None,
Some(num) => Some(num + 1),
}
}
fn main(){
let num2: Option<i32> = Some(32);
// 调用函数执行匹配逻辑
dbg!(plus_one(num2));
}
match
匹配要求我们覆盖所有可能的模式。这样的匹配是无穷尽的。
假设我们只处理某些匹配,其他按默认逻辑处理就好。就需要使用other
fn plus_two(val: i32) -> i32 {
match val {
3 => 3 + 2,
10 => 10 + 5,
other => other - 1,
}
}
fn main(){
dbg!(plus_two(10)); // 15
dbg!(plus_two(4)); // 3
}
如果不想使用匹配的值,通过_
处理。
fn plus_two(val: i32) -> i32 {
match val {
3 => 3 + 2,
10 => 10 + 5,
_ => -1,
}
}
除了匹配 3、10,其他值都默认返回-1.
通过other / _
穷举了所有可能的情况。保证了程序的安全性。
if let
丢弃掉match的无穷尽枚举匹配
通过if let
可以仅处理需要匹配处理逻辑的模式,忽略其他模式,而不是使用match的other/_
fn main(){
let mut num = 3;
if let 3 = num {
num += 2;
}
dbg!(num); // 5
}
打印输出
在以上的示例中,我们都是使用 print!
或者println!
来打印输出。基本类型中基本都是可以打印输出的。
但其他一些则不能打印输出,比如:元组、数组、结构体等。
let a = 32; // 正常打印输出 32
let arr = [3,4,5,6];
println!("{}",arr);
错误打印输出:
根据错误提示,可以看到书写提示。{}
替换为{:?}或{:#?}
let arr = [3,4,5,6];
println!("{:?}",arr);
再看下结构体的 打印输出
// 直接打印之前定义的User实例
println!("{:?}", user)
又报错了,看错误提示:
需要增加属性来派生Debug trait。才可以打印结构体实例。
#[derive(Debug)]
struct User {
name: String,
age: i32,
email: String,
id: u64,
}
fn main(){
println!("{:?}", user)
}
需要注意的是,如果当前这个实例被用来生成其他实例,则其中某些字段的所有权已被转移。
dbg!
宏
与println!
不同,它会接收这个表达式的所有权。println!
是引用
let user = User {
name: String::from("hboot"),
age: 32,
email: String::from("bobolity@163.com"),
id: 3729193749,
};
dbg!(user);
与println!不同的是,会打印出代码行号。如果不希望转移所有权,则可以传一个引用dbg!(&user)