rust 初识基础: 变量、数据类型、函数、所有权、枚举

发布时间 2023-05-28 22:55:15作者: hboot

了解到 rust 和 WebAssembly 的结合使用,可以构建前端应用,而且性能也比较好。初步学习使用
rust 是预编译静态类型语言。

安装 rust

官网下载 rust-CN , 大致了解下为什么选择:高性能、可靠性、生产力。

打开控制台啊,执行安装 (mac 系统,windwos 或其他系统查看官网)

&> curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

安装成功时,会打印:

rust-install.png

或则会通过查看版本检查是否安装更新:

$> rustc --version

有打印就成功了。

rust 通过 rustup 来管理。更新 rust

  • rustup update 更新 rust
  • rustup self uninstall 写在 rust 和 rustup 管理器。

rust 一些常用的包依赖 c 语言,需要安装 C 编译器

如果你不是一个 c/c++开发者,则一些常用的 rust 在使用时,会报错。根据不同的系统需要安装 c 编译器。

  • mac 系统安装xcode-select
$> xcode-select --install
  • windwos 系统则需要安装visual studio

visualstudio

不安装时,报错如图:

start-error.png

起步项目 hello world

rust 的主程序代码文件都以.rs结尾

$> mkdri rust-web
$> cd rust-web
$> vi main.rs

编辑main.rs文件,写入以下内容

fn main() {
    println!("Hello, world!");
}

编译文件并执行

$> rustc main.rs
$> ./main

可以看到控制打印输出

hello-world.png

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-success.png

创建一个项目,cargo new rust-web ; 因为我已经创建了项目目录,所以使用cargo init进行初始化

初始化完目录如下:

init-project.png

Cargo.toml 为项目的清单文件。包含了元数据信息以及项目依赖的库,在 rust 中,所有的依赖包称为crates

[package]
name = "rust-web"
version = "0.1.0"
edition = "2021"

[dependencies]

src目录则是项目功能文件目录,可以看到文件后缀名是.rs

fn main() {
    println!("Hello, world!");
}

启动执行cargo run, 如果启动成功,就会打印出来

start-success.png

cargo build构建编译,可以在target目录下看到,

通过执行./target/debug/rust-web,可以看到和上面输出同样的内容;

rust 基础语法

学习一门新语言,首先要掌握它的语法,怎么去写、表达。

变量、基本类型、函数、注释和控制流

变量与可变性

let定义一个变量,更改后打印输出。

fn main() {
    let age = 24;
    print!("{age}");
    age = 34;
    print!("{age}");
}

执行cargo check,可以看到打印输出,不允许更改。

immutable-error.png

如果需要变更,则需要添加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$的数字。

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

错误打印输出:

print-error.jpg

根据错误提示,可以看到书写提示。{}替换为{:?}或{:#?}

let arr = [3,4,5,6];

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

再看下结构体的 打印输出


// 直接打印之前定义的User实例 
println!("{:?}", user)

又报错了,看错误提示:

struct-error.jpg

需要增加属性来派生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)