Rust基础入门

2024-11-05 Rust Web3基础

各个语言其实大同小异,有些语法奇特,有些流畅自然,各有特点,但都差不多。不用担心语法记不住,有个大致的理解就行了,需要的时候再回来翻,只需要关注语言之间的区分,以及该语言独有的一些特性即可

参考文档:

https://rustwiki.org/zh-CN/book/title-page.html

https://www.hackquest.io/zh

 

Rust基础

安装地址:https://www.rust-lang.org/zh-CN/tools/install

可以通过查看version的方式检查是否装好了rust

rustc --version

你可以编写一个main.rs,输入以下代码

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

然后执行编译、运行这两个命令,就可以看到

rustc main.rs

 

Cargo入门

Cargo 是 Rust 的构建系统和包管理器。大多数人使用 Cargo 来管理他们的 Rust 项目,因为它可以为你处理很多任务,比如构建代码、下载依赖库,以及编译这些库。(我们把代码所需要的库叫做依赖dependency))。

最简单的 Rust 程序(如我们刚刚编写的)不含任何依赖。所以如果使用 Cargo 来构建 “Hello, world!” 项目,将只会用到 Cargo 构建代码的那部分功能。在编写更复杂的 Rust 程序时,你将添加依赖项,如果使用 Cargo 启动项目,则添加依赖项将更容易。

cargo --version

cargo 创建项目

cargo new hello_cargo

toml 样例

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

[dependencies]

src/main.rs

fn main() {
    println!("Hello, world!");
}
  • cargo build 构建项目
  • cargo run 运行项目
  • cargo check 检查代码确保其可以编译,但并不产生可执行文件
  • cargo build --release 来优化编译项目

 

变量可变性

Rust中的变量可以分为可变变量不可变变量,通俗来说,就是变量的值是否允许修改,如果在内存中给变量分配完值之后,可以根据需要来修改这个值,那它就是可变变量,反之,不允许这个值再发生变更,那就是不可变变量。前者很容易理解,变量的值在不同场景下会发生变化,它为编程提供了灵活性;后者,也有存在的道理,通过不可变的限制,提供了编程的安全性,特别是在多线程环境下,也减少了一些运行时检查,在一定程度上提升运行性能。

注意:这里提到的是否可变指的是变量的值,而非变量的数据类型。另外,不可变变量这个特性并不是Rust特有的,在Java中也有类似的概念,比如String类型就是不可变变量,当对不可变对象的值进行了类似的修改操作时,本质上是创建了新的对象,而不是修改了这个不可变变量的值。

如果一个变量你不需要它可变,那么你定义为可变变量就会损失一定的性能,因为可变变量可以随时改变值,编译器无法进行某些优化。而如果一个变量你需要它可变,你却声明为不可变,那么你只能通过不断声明新的同名变量来模拟可变,但实际在内存中是创建了一个新的对象,这个时候就会降低代码性能。

fn main() {
    // x 为可变变量,mut即 mutable的意思,该修饰符修饰的变量允许改变
    let mut x = 1;
    println!("x = {}", x); 
    x = 2;
    println!("x = {}", x);

    // y 为不可变变量,如果没有指定mut,则Rust默认为不可变
    let y = 3;
    println!("y = {}", y);
    // 对不可变变量 y 重新赋值,Rust编译器会给出cannot assign twice to immutable variable y的错误提示
    y = 4; 
    println!("y = {}", y);
}

与不可变变量类似,常量constant)是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异。常量不允许使用 mut。常量不仅仅默认不可变,而且自始至终不可变。常量使用 const 关键字而不是 let 关键字来声明,并且值的类型必须注明。

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

 

数值类型

Rust 基本数据类型中最常用的数值类型:有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)。

整数 是 没有小数部分的数字,具体有如下几种类型:

image-20241028212608764

表示方式为 有无符号 + 类型大小(位数),i 是英文单词 integer 的首字母,代表有符号类型,包含负整数、0和正整数,与之相反的是 u,代表无符号 unsigned 类型,包含0和正整数。Rust默认的整数类型是 i32,即用32个bit位表示有符号的整数。

浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32 和 f64,分别为 32 位和 64 位大小。默认浮点类型是 f64。

// 这里a为默认的 i32 类型
let a = 1;
// 可以指定也可以指定为具体的整数类型
let b: u32 = 1;

// 这里c为默认的 f64 类型
let c = 1.0;
// 也可以指定为具体的浮点数类型
let d: f32 = 1.0;

// Rust中可以方便的使用不同进制来表示数值,总有一款适合你
let x: i32 = 100_000_000;
let y: i32 = 0xffab;
let z: i32 = 0o77;
let m: i32 = 0b1111_0000;
let n: u8 = b'A';
println!("x = {}, y = {}, z = {}, m = {}, n = {}", x, y, z, m, n);

 

整型溢出:比方说有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生整型溢出integer overflow),这会导致两种行为的其中一种。当在调试(debug)模式编译时,Rust 会检查整型溢出,若存在这些问题则使程序在编译时 panic

在当使用 --release 参数进行发布(release)模式构建时,Rust 检测会导致 panic 的整型溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码包裹(two’s complement wrapping)的操作。简而言之,大于该类型最大值的数值会被“包裹”成该类型能够支持的对应数字的最小值。比如在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。

要显式处理溢出的可能性,可以使用标准库针对原始数字类型提供的以下一系列方法:

  • 使用 wrapping_* 方法在所有模式下进行包裹,例如 wrapping_add
  • 如果使用 checked_* 方法时发生溢出,则返回 None
  • 使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
  • 使用 saturating_* 方法使值达到最小值或最大值

 

字符和布尔类型

字符类型是用char类型表示的,占用4个字节的空间,可以表示Unicode字符集中的任何字符,包括ASCII字符、各种符号、各种语言的文字,甚至是表情符号。通过单引号'可以创建一个char类型的值。例如let a:char = '🦀';

布尔类型有两种值:true 和 false,占用内存的大小为 1 个字节。

注意:这里是介绍的是字符(用单引号''表示),不是字符串(用双引号""表示),在 Rust 中,字符串类型的长度取决于使用的编码集,默认情况下,Rust 使用 UTF-8 编码,一个字符占用 1~4 个字节,而 char 类型占用 4 个字节的存储空间,即使有些字符在特定编码集下只需要 1~3 个字节表示,Rust 也会将其扩展为 4 个字节。这样做的好处是:

  • 保证所有 char 值在内存中占用固定大小,有利于内存对齐和访问效率。

  • 避免编码转换开销,直接使用 4 字节值可以高效处理字符。

  • 足够表示 Unicode 标量值所需的全部码位,确保未来的兼容性。

 

流程控制

if 表达式与赋值

  • if 语句块可以作为表达式返回值
  • 所有分支返回值类型必须相同
  • 每个分支最后一个表达式作为返回值
fn main() {
    let condition = true;
    // if 语句块作为表达式返回值
    let number = if condition {
        5
    } else {
        6
    };
    println!("The value of number is: {}", number);
}

 

循环控制(continue 和 break)

fn main() {
    for item in 1..=5 {
        if item == 2 {
            continue;  // 跳过当前循环
        }
        if item == 4 {
            break;    // 终止整个循环
        }
        println!("this Item is : {}", item);
    }
}

 

for 循环与所有权

  • into_iter() 转移所有权
  • iter() 创建不可变引用的迭代器
  • iter_mut() 创建可变引用的迭代器
fn main() {
    // 所有权转移的情况
    let vec1 = vec![1, 2, 3, 4, 5];
    for item in vec1.into_iter() {  // 获取所有权
        println!("Item: {}", item);
    }
    // vec1 的所有权已转移,这里无法使用

    // 借用的情况
    let vec2 = vec![1, 2, 3, 4, 5];
    for item in &vec2 {  // 借用引用
        println!("Item: {}!", item);
    }
    println!("{:?}", vec2);  // vec2 仍可使用

    let vec3 = vec![1, 2, 3, 4, 5];
    for item in vec3.iter() {  // 借用引用
        println!("Item: {}!", item);
    }
    println!("{:?}", vec3);  // vec3 仍可使用

    let mut vec4 = vec![1, 2, 3, 4, 5];
    for item in vec4.iter_mut() {  // 借用引用
        println!("Item: {}!", item);
    }
    println!("{:?}", vec4);  // vec4 仍可使用
}

 

while 和 for 循环的比较

  • while 循环需要手动管理索引
  • for 循环更安全,避免索引越界
  • for 循环性能更好,减少运行时检查
fn main() {
    let array = [10, 20, 30, 40, 50];

    // while 循环使用索引
    let mut index = 0;
    while index < 5 {
        println!("value: {}", array[index]);
        index += 1;
    }

    // for 循环使用迭代器
    for element in array.iter() {
        println!("value: {}", element);
    }
}

 

loop 循环与返回值

  • loop 创建无限循环
  • break 可以返回值
  • 适用于重试逻辑或需要返回值的循环
fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;  // 返回值
        }
    };
    println!("Result: {}", result);
}

 

枚举

枚举(Enum)是一种用户自定义数据类型,它允许你列举一些可能的值,也叫变体(variants),每个变体也可以包含不同类型的数据。枚举主要用于表示不同种类的选项或操作,以及进行模式匹配等场景。它的定义如下:

// 简单枚举
enum TrafficLight {
    Red,
    Yellow,
    Green,
}

// 包含值的枚举,不同成员可以持有不同的数据类型
enum TrafficLightWithTime {
  Red(u8),
  Yellow(char),
  Green(String),
}

fn main() {
    // 通过 :: 操作符来访问 TrafficLight 的成员
    let red = TrafficLight::Red;
    let yellow = TrafficLight::Yellow;

    // 包含时间的红绿灯
    let red_with_time = TrafficLightWithTime::Red(10);
    let yellow_with_time = TrafficLightWithTime::Yellow('3');
    let green_with_time = TrafficLightWithTime::Green(String::from("绿灯持续30秒"));
}

 

什么是Option枚举,如何使用?

Option 枚举主要用于处理可能出现空值的情况,以避免使用空指针引起的运行时错误。它的定义如下:

// 它有两个枚举值,Some(T): 包含一个具体的值 T,以及None: 表示没有值。
enum Option<T> {
	None,
	Some(T),
}

下面的例子中divide函数的返回值就是Option枚举。请注意:Rust 的 标准库prelude 中,Option 枚举是默认导入的,因此在代码中使用 Option 时无需显式使用 Option:: 前缀或者通过 use 语句显式导入。

// 定义一个函数,返回一个Option枚举
fn divide(x: f64, y: f64) -> Option<f64> {
    if y == 0.0 {
        None
    } else {
        Some(x / y)
    }
}

 

Some 是 Rust 中 Option 枚举类型的一个变体

// Option 的定义(标准库中)
enum Option<T> {
    Some(T),    // 包含一个值
    None        // 表示空值
}

fn main() {
    // 创建一些 Option 实例
    let some_number = Some(5);    // Some(T) 包含一个数字
    let some_string = Some("hello");  // Some(T) 包含一个字符串
    let no_number: Option<i32> = None;   // 没有值

    // 使用 match 处理 Option
    match some_number {
        Some(n) => println!("有一个数字: {}", n),
        None => println!("没有数字"),
    }

    // 使用 if let 简化匹配
    if let Some(n) = some_number {
        println!("数字是: {}", n);
    }
}

 

模式匹配

模式匹配:允许我们将一个target与一系列的模式相比较,并根据相匹配的模式执行对应的表达式。Rust 中常见的模式匹配有 match 和if let 两种,这里我们以 match 举例来看看什么是模式匹配。

match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

 

举个几例子,展示下 match模式匹配的其他用法:

  1. 定义 Shape 枚举,包含 Circle、Rectangle 和 Square 三种形状,通过 match 表达式在 calculate_area 函数中计算不同形状的面积
// 定义形状枚举,每个变体包含需要的数据
enum Shape {
    Circle(f64),    // 圆形,包含半径
    Rectangle(f64, f64),  // 矩形,包含宽和高
    Square(f64),    // 正方形,包含边长
}

// 计算面积的函数
fn calculate_area(shape: &Shape) -> f64 {
    match shape {
        // 通过模式匹配解构出每个形状的数据
        Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
        Shape::Rectangle(width, height) => width * height,
        Shape::Square(side) => side * side,
    }
}

fn main() {
    // 创建不同的形状实例
    let circle = Shape::Circle(3.0);
    let rectangle = Shape::Rectangle(4.0, 5.0);
    let square = Shape::Square(2.0);

    // 计算并打印面积
    println!("圆形的面积:{}", calculate_area(&circle));
    println!("矩形的面积:{}", calculate_area(&rectangle));
    println!("正方形的面积:{}", calculate_area(&square));
}

 

  1. 直接使用 match 进行值绑定
fn main() {
    let circle = Shape::Circle(3.0);
    
    // 使用 match 表达式直接计算面积并赋值
    let area = match circle {
        Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
        Shape::Rectangle(width, height) => width * height,
        Shape::Square(side) => side * side,
    };
    println!("圆形的面积:{}", area);
}

 

  1. 结构体模式匹配:
// 定义坐标点结构体
struct Point {
    x: i32,
    y: i32,
}

// 处理坐标点的函数
fn process_point(point: Point) {
    match point {
        // 精确匹配原点
        Point { x: 0, y: 0 } => println!("坐标在原点"),
        // 使用变量绑定匹配任意点
        Point { x, y } => println!("坐标在 ({}, {})", x, y),
    }
}

fn main() {
    let point1 = Point { x: 0, y: 0 };
    let point2 = Point { x: 3, y: 7 };
    
    process_point(point1);  // 输出:坐标在原点
    process_point(point2);  // 输出:坐标在 (3, 7)
}

 

方法

方法是与结构体 structs 或枚举 enums 或特征对象 trait 等特定类型相关联的函数,它们允许你在这些类型上定义行为,并且支持像调用普通函数一样调用该行为。

方法与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,以及对应的函数体逻辑。不过方法与函数是不同的,因为方法通过 impl 关键字在结构体或枚举的上下文中定义,并且它们第一个参数总是 &self,代表调用该方法的结构体实例。

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

impl Rectangle {
    /* 同名方法:使得代码更加一致和简洁。当你需要获取或者设置结构体的属性时,
     * 可以直接使用属性名称作为方法名,而不需要额外记忆或查阅文档,同时也更符
     * 合直观的阅读和理解方式,降低代码的维护难度。
     */
    // width方法的第一个参数为 &self,代表结构体实例本身
    pub fn width(&self) -> u32 {
        return self.width;
    }

    // 关联函数,没有 &self 参数
    pub fn new(width: u32, height: u32) -> Self {
        Rectangle { width, height }
    }
}

fn main() {
    // 方法中没有 self 参数,则该方法为关联函数(associated functions)
    // 通常用于初始化实例的场景,调用关联函数 new 来创建结构体对应的实例
    let rect1 = Rectangle::new(30, 50);

    // 方式一、访问 Rectangle 的 width字段
    println!("{}", rect1.width);

    // 方式二、调用Rectangle 的 width 方法,类似于getter()
    println!("{}", rect1.width());
}

 

函数

Rust 代码中的函数和变量名使用 snake_case 规范风格,所有字母都是小写并使用下划线分隔单词。

现在我们来看下函数的各个组成部分。需要注意的是,函数的参数需要显式的标注类型,不仅有助于提高代码的可读性,也有助于 Rust 提供更强的类型安全性,帮助编译器在类型不匹配时发现错误,提供有用的错误信息。

// fn 为声明函数的关键字
// unsafe_add()是函数名,函数的命名要遵循 snake_case 的规范,同时要见名知意,提高代码的可读性
// i 和 j 是入参,并且需要显式指定参数类型
// --> i32 表明出参也是 i32 类型
fn unsafe_add(i: i32, j: i32) -> i32 {
   // 表达式形式,所以函数会在计算求和后返回该值
   i + j
}

 

复合数据类型

动态字符串切片

切片(slice):是一种引用数据结构,它允许你引用数据的一部分而不需要拷贝整个数据。切片通常用于数组、字符串等集合类型。

**字符串切片(String slice):**是一种特殊的切片,专门用于处理字符串。字符串切片的类型是 &str。它可以通过索引或范围来指定字符串的一部分。字符串切片提供了对字符串的引用,而不引入额外的内存开销。

// 该字符串分配在内存中
let s = String::from("hello world");

// hello 没有引用整个 String字符串 s,而是引用了 s 的一部分内容,通过 [0..5] 的方式来指定。
let hello: &str = &s[0..5];
let world: &str = &s[6..11];

 

image-20241105231946644

 

如果字符串包含汉字,在获取字符串切片时有什么要注意的?

字符串切片的索引位置是按照字节而不是字符。由于汉字使用 UTF-8 编码,一个汉字(字符)可能由一个或多个字节组成。因此索引必须对应一个完整的汉字的边界,否则获取该汉字会失败。

fn main() {
    let s: String = String::from("hello, hackquest.");

    // 起始从0开始, .. 代表一个或多个索引
    let slice1: &str = &s[0..2];
    // 默认也是从0开始
    let slice2: &str = &s[..2];

    let len: usize = s.len();
    // 包含最后一个字节,由于最后1个字节的索引为(len-1),所以[4..len]的方式刚好包含了第(len-1)个字节
    let slice3: &str = &s[4..len];
    // 默认到最后1个字节
    let slice4: &str = &s[4..];

    // 获取整个字符串的切片
    let slice5: &str = &s[0..len];
    // 同上
    let slice6: &str = &s[..];
}

 

字符串字面量

字符串字面量(String Literals):是指在代码中直接写死的、由双引号包围的一系列字符,例如 "Hello, world!"。它硬编码到最终的程序二进制中,类型为 &str,跟我们上节的动态字符串切片类型一样,因为它也是一种字符串引用,对程序二进制文件中静态分配的字符串数据的引用,也称之为静态字符串切片。

 

字符串字面量 和 动态字符串切片的异同点?

相同点

  1. 都是都是对字符串数据的引用,而不是实际的字符串数据本身
  2. 都是 UTF-8 编码的字符串
  3. 两者都可以使用一些相似的字符串操作,如切片、查找、比较等。

 

不同点

  1. 字符串字面量被硬编码到程序二进制文件中,因此在整个程序运行期间有效。而字符串切片取决于引用它的变量或数据结构的生命周期。
  2. 字符串字面量在编译时已知大小,是固定大小的;字符串切片在运行时确定大小,是动态大小的。
// 字符串字面量
let a: &str = "hello, hackquest.";
println!("{} go go go !", a);
// Rust在编译时就会把变量a硬编码到程序中,所以编译后的第5行代码应该长这个样子。
// println!("hello, hackquest. go go go !");

// 通过索引获取 hello
let b = &a[..5];
println!("{}", b);

// 获取动态字符串切片
let s1: String = String::from("hello,world!");
let s2: &str = &s1[..5];
println!("{}", s2);

// 动态字符串转成字符串字面量
let s3: &str = s1.as_str();

// 字符串字面量转成动态字符串类型
let s4: String = "hello,world".to_string();
let s5: String = String::from("hello,world");

 

动态字符串操作

字符串的追加 (Push)、插入 (Insert)、替换 (Replace)、删除 (Delete)操作

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

    // 追加字符串,修改原来的字符串,不是生成新的字符串
    s.push_str("rust");
    println!("追加字符串 push_str() -> {}", s);

    // 追加字符
    s.push('!');
    println!("追加字符 push() -> {}", s);

    // 插入字符,修改原来的字符串,需要指定索引位置,索引从0开始,
    // 如果越界则会发生错误
    s.insert(5, ',');
    println!("插入字符 insert() -> {}", s);

    // 插入字符串
    s.insert_str(6, " I like");
    println!("插入字符串 insert_str() -> {}", s);

    // replace 替换操作生成新的字符串。需要2个参数,第一个参数是
    // 要被替换的字符串,第二个参数是新的字符串
    let str_old = String::from("I like rust, rust, rust!");
    let str_new = str_old.replace("rust", "RUST");
    println!("原字符串长度为:{},内存地址:{:p}", str_old, &str_old);
    println!("新字符串长度为:{},内存地址:{:p}", str_new, &str_new);

    // pop 删除操作,修改原来的字符串,相当于弹出字符数组的最后一个字符
    // 返回值是删除的字符,Option类型,如果字符串为空,则返回None
    // 注意:pop是按照“字符”维度进行的,而不是“字节”
    let mut string_pop = String::from("删除操作,rust 中文!");
    // 此时删除的是末尾的感叹号“!”
    let p1 = string_pop.pop();
    println!("p1:{:?}", p1);
    // 在p1基础上删除末尾的“文”
    let p2 = string_pop.pop();
    println!("p2:{:?}", p2);
    // 此时剩余的字符串为“删除操作,rust 中”
    println!("string_pop:{:?}", string_pop);
}

 

元组

fn main() {
    // 创建1个长度为4,多种不同元素类型的元组
    let tup: (i32, f64, u8, &str) = (100, 1.1, 1, "这是一个元组");
    
    // 解构tup变量,将其中第2个元素绑定给变量x
    let (_, x, ..) = tup;
    println!("The value of x is: {}", x);  //1.1

    // 使用.来访问指定索引处的元素
    let first = tup.0;
    let second = tup.1;
    let third = tup.2;
    let fourth = tup.3;
    println!("The value of first is: {}, second is: {}, third is: {}, fourth is: {}", first, second, third, fourth);

    let s = String::from("hello, hackquest.");
    // 函数返回值为元组类型
    let (s1, len) = calculate_length(s);
    println!("The length of '{}' is {}.", s1, len);
}

// len() 返回字符串的长度
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();

    (s, length)
}

 

结构体

结构体:是一种自定义数据类型,用于组织和存储不同类型的数据成员。它允许你创建一个包含多个字段的数据结构,每个字段都有自己的类型和名称,使得代码更具可读性和可维护性。通过struct关键字定义。

#[derive(Debug)] // Debug trait 允许我们以调试格式打印数据结构
struct Car {
    // 品牌
    brand: String,
    // 颜色
    color: String,
    // 生产年份
    year: String,
    // 是否新能源
    is_new_energy: bool,
    // 价格
    price: f64
}

// 标准的创建方式
fn build_car(color: String, year: String, price: f64) -> Car {
    Car {
        brand: String::from("Tesla"),
        color: color,
        year: year,
        is_new_energy: true,
        price: price,
    }
}

// 简化后的创建方式
fn build_car2(color: String, year: String, price: f64) -> Car {
    Car {
        brand: String::from("Tesla"),
        // 函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化
        color,
        year,
        is_new_energy: true,
        price,
    }
}

fn main() {
    // 声明 Car 类型变量(要求变量必须是 mut 类型)
    let mut car1 = build_car2(String::from("black"), String::from("2023-01-01"), 123.00);

    // 访问并修改结构体(通过 . 操作符访问)
    car1.color = String::from("white");
    println!("car1 {:?}", car1);

    // 根据已有的结构体实例,创建新的结构体实例
    let car2 = Car {
        color: String::from("greey"),
        // 其他字段从car1中取,..car1 必须在结构体的尾部使用
        ..car1
    };

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

 

元组结构体(Tuple Struct)

结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体。

如果你希望有一个整体名称,但是又不关心里面字段的名称。例如下面的 Point 元组结构体,众所周知 3D 点是 (x, y, z) 形式的坐标点,因此我们无需再为内部的字段逐一命名为:x, y, z。

// 只有结构体名称:Color,没有字段名称
struct Color(i32, i32, i32);
// 只有结构体名称:Point,没有字段名称
struct Point(i32, i32, i32);

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

 

   

静态数组

数组:是将多个相同类型的元素依次组合在一起,形成的集合。在 Rust 中常用的数组有两种,一种是直接分配在栈内存中,速度很快但是长度固定的静态数组 array,另一种则是分配在堆内存中,可动态增长,但有性能损耗的动态数组 Vector。

fn main() {
    // array = [类型; 长度] 这种语法对于i32、f64、bool等基础类型是OK的
    let a = [3u8; 5]; // a = [3, 3, 3, 3, 3]

    // 但是对于String这类非基础类型,需要用如下方式,因为基础类型数据是在栈内存,可以直接拷贝,
    // 而非基础类型的数据是在堆内存,需要深拷贝。
    let b: [String; 3] = std::array::from_fn(|_i| String::from("rust")); // b = ["rust","rust","rust"]

    let c = [9, 8, 7, 6, 5];
    // 通过下标直接访问
    let first = c[0]; // first = 9
    let second = c[1]; // second = 8

    // 访问不存在的元素,编译器会直接识别到并给出错误提示
    // let none_element = c[100];

    // arrays是一个二维数组,其中每一个元素都是一个数组,元素类型是[u8; 5]
		// arrays = [[3, 3, 3, 3, 3],[9, 8, 7, 6, 5]]
    let arrays: [[u8; 5]; 2] = [a, c];
}

 

动态数组

**动态数组 ** Vec<T> 是一种灵活的数据结构,允许在运行时动态改变大小。所以它的长度是可变的,可以根据需要动态增加或减少元素。这为处理不确定数量的数据提供了便利,比如读取未知数量的用户输入或动态生成数据集。

与String类型不同,动态数组 Vec<T> 是通用的,可以存储“任何类型”的元素,而 String 专门用于处理UTF-8编码的文本数据。动态数组 Vec 提供更灵活的操作,但在处理文本时,String 提供了一些额外的字符串特定功能,例如字符串连接、切片等。选择使用动态数组 Vec 还是 String 取决于具体的需求和数据类型。

下面的代码展示了5种创建动态数组的不同方式。

// 1.显式声明动态数组类型
let v1: Vec<i32> = Vec::new();

// 2.编译器根据元素自动推断类型,须将 v 声明为 mut 后,才能进行修改。
let mut v2 = Vec::new();
v2.push(1);

// 3.使用宏 vec! 来创建数组,支持在创建时就给予初始化值
let v3 = vec![1, 2, 3];

// 4.使用 [初始值;长度] 来创建数组,默认值为 0,初始长度为 3
let v4 = vec![0; 3];  // v4 = [0, 0, 0];

// 5.使用from语法创建数组
let v5 = Vec::from([0, 0, 0]);
assert_eq!(v4, v5);

 

下面的代码中展示了数组Vector的一些常用操作(访问、修改、插入、删除等)。

fn main() {
    let mut v1 = vec![1, 2, 3, 4, 5];

    // 通过 [索引] 直接访问指定位置的元素
    let third: &i32 = &v1[2];
    println!("第三个元素是 {}", third);

    // 通过 .get() 方法访问,防止下标越界
    // match属于模式匹配,后续章节会有详细介绍
    match v1.get(2) {
        Some(third) => println!("第三个元素是 {third}"),
        None => println!("指定的元素不存在"),
    }

    // 迭代访问并修改元素
    for i in &mut v1 {
        // 这里 i 是数组 v 中元素的可变引用,通过 *i 解引用获取到值,并 + 10
        *i += 10
    }
    println!("v1 = {:?}", v1);    // v1 = [11, 12, 13, 14, 15]

    let mut v2: Vec<i32> = vec![1, 2];
    assert!(!v2.is_empty()); // 检查 v2 是否为空
    v2.insert(2, 3); // 在指定索引插入数据,索引值不能大于 v 的长度, v2: [1, 2, 3]
    assert_eq!(v2.remove(1), 2); // 移除指定位置的元素并返回, v2: [1, 3]
    assert_eq!(v2.pop(), Some(3)); // 删除并返回 v 尾部的元素,v2: [1]
    v2.clear(); // 清空 v2, v2: []
}

 

动态数组Vector在内存中的结构是什么样的,如何进行动态调整的?

fn main() {
    let mut v: Vec<i32> = vec![1, 2, 3, 4];
    //prints 4,即数组的初始容量是4
    println!("v's capacity is {}", v.capacity());
    // 打印内存地址
    println!("Address of v's first element: {:p}", &v[0]);
    
    v.push(5);
    //prints 8,数组进行扩容,容量变成8
    println!("v's capacity is {}", v.capacity());
    // 打印扩容后的内存地址,会发现跟上面的地址并不相同
    println!("Address of v's first element: {:p}", &v[0]);
}

初始时动态数组v的容量是4,堆内存中存储数值,栈内存中记录了堆内存的地址指针、数组容量及数组大小,当添加新元素5时,数组进行扩容,重新申请一块 2 倍大小的内存(即8),再将所有元素拷贝到新的内存位置,同时更新指针数据,这时数组大小是5,容量大小是8。

image-20241028225247737

image-20241028225304135

HashMap

Hashmap 是 Rust 语言中的一个集合类型,用于存储键与值(key-value)的对应关系。每个键key映射到一个值value,键必须是唯一的。这种结构允许我们通过键快速检索到值,而不需要遍历整个集合。Hashmap 的高效性来自于它的散列函数,这个函数能够将键转换成存储位置的索引,从而直接访问内存中的位置,这样就大大加快了查找速度。

可以使用如下2种方式创建 HashMap,如果预先知道要存储的 key-value 对个数,可以创建指定大小的 HashMap,避免频繁的内存分配和拷贝,提升性能。

// 由于 HashMap 并没有包含在 Rust 的 prelude 库中,所以需要手动引入
use std::collections::HashMap;
fn main() {
    // 创建一个HashMap,用于存储学生成绩
    let mut student_grades = HashMap::new();
    student_grades.insert("Alice", 100);
    
    // 创建指定大小的 HashMap,避免频繁的内存分配和拷贝,提升性能。
    let mut student_grades2 = HashMap::with_capacity(3);
    student_grades2.insert("Alice", 100);
    student_grades2.insert("Bob", 99);
    student_grades2.insert("Eve", 59);
}

 

数组如何转成 HashMap,以及 HashMap的查询、遍历、更改等操作。

use std::collections::HashMap;
fn main() {
    // 动态数组,类型为元组 (用户,余额)
    let user_list: Vec<(&str, i32)> = vec![
        ("Alice", 10000),
        ("Bob", 1000),
        ("Eve", 100),
        ("Mallory", 10),
    ];

    // 使用迭代器和 collect 方法把数组转为 HashMap
    let mut user_map: HashMap<&str, i32> = user_list.into_iter().collect();
    println!("{:?}", user_map);

    // 通过 hashmap[key] 获取对应的value
    let alice_balance = user_map["Alice"];
    println!("{:?}", alice_balance);

    // 通过 hashmap.get(key) 获取对应的value,返回值为 Option 枚举类型
    let alice_balance: Option<&i32> = user_map.get("Alice");
    println!("{:?}", alice_balance);

    // 不存在的key,返回值为 None,但不会报错
    let trent_balance: Option<&i32> = user_map.get("Trent");
    println!("{:?}", trent_balance);

    // 覆盖已有的值,insert 操作 返回旧值
    let old = user_map.insert("Alice", 20000);
    assert_eq!(old, Some(10000));

    // or_insert 如果存在则返回旧值的引用;如果不存在,则插入默认值,并返回其引用
    let v = user_map.entry("Trent").or_insert(1);
    assert_eq!(*v, 1); // 不存在,插入1

    // 验证Trent对应的值
    let v = user_map.entry("Trent").or_insert(2);
    assert_eq!(*v, 1); // 已经存在,因此2没有插入
}

 

项目结构

module:用于组织和封装代码的单元。它可以包含函数、结构体、枚举、常量、Trait等,也可以包含其他模块。通过使用mod关键字可以在 Rust 中创建模块,并且模块可以嵌套形成模块树。

crate:是 Rust 中的编译单元,可以是库 crate二进制 crate(它们的区别参见FAQ)。一个 crate 可以包含一个或多个模块。

package:是一个包含一个或多个相关 crate 的项目工程。它由一个 Cargo.toml文件定义,该文件包含有关项目的元信息、依赖关系以及其他配置选项。一个 package 可能包含一个主 crate(通常是可执行文件)和零个或多个库 crate(通常是依赖 crate)。

下面的例子演示了模块(module)的层次结构,构建了一个 crate,最终组成了一个 package。通过模块的层次组织,代码具有良好的结构和可读性。

// src/main.rs

// 主模块,相当于房子的大厅
mod living_room {
    // 子模块,相当于房子的卧室
    mod bedroom {
        // 模块中的函数,相当于卧室中的家具
        pub fn sleep() {
            println!("Sleeping in the bedroom");
        }
    }

    // 子模块,相当于房子的厨房
    mod kitchen {
        // 模块中的函数,相当于厨房中的设备
        pub fn cook() {
            println!("Cooking in the kitchen");
        }
    }

    // 主模块中的函数,相当于大厅中的活动
    pub fn relax() {
        println!("Relaxing in the living room");
        bedroom::sleep(); // 调用卧室模块中的函数
        kitchen::cook();  // 调用厨房模块中的函数
    }
}

// 主函数,相当于整个房子的入口
fn main() {
    // 调用 living_room 模块中的函数
    living_room::relax();
}

 

详细介绍 Rust 中库 crate 和二进制 crate的概念?

库 crate 和二进制 crate 是两种不同类型的 Rust 项目。它们分别用于构建库(用于被其他程序引用)和可执行程序。我们分别看下它们的概念及区别:

库 crate :是一种 Rust 项目,通过cargo new --lib 库名来创建,它的主要目的是提供可供其他程序引用的功能性代码。库 crate 的代码通常是一些通用的、可复用的功能。

组织方式: 一个库 crate 的代码通常位于一个名为lib.rs的文件中,该文件包含 crate 的主模块。库 crate 的代码可以由其他 crate 引用,通过在 Cargo.toml 文件中添加相应的依赖。

二进制 crate:也是一种 Rust 项目,通过cargo new 项目名来创建,它的主要目的是构建可执行程序。这个 crate 可以包含多个模块,其中一个模块被指定为主入口点,例如 main.rs 文件。二进制 crate 的代码用于创建独立的可执行文件。

组织方式: 一个二进制 crate 的代码通常包含在一个名为 main.rs 的文件中,该文件包含程序的主函数 main()。

总的来说,库 crate,用于构建可供其他程序引用的功能性代码;代码通常位于 lib.rs 文件中;可以被其他 crate 引用作为依赖。而二进制 crate,用于构建可执行程序;代码通常位于 main.rs 文件中;产生一个独立的可执行文件。

注意: 在一个 Rust 项目中,你可以同时包含库 crate 和二进制 crate。例如,一个项目可能包含一个库用于提供通用功能,同时也包含一个可执行程序用于演示或测试该库。在这种情况下,项目的目录结构通常包含 src/lib.rs 和 src/main.rs。

作者:加密鲸拓

版权:此文章版权归 加密鲸拓 所有,如有转载,请注明出处!