各个语言其实大同小异,有些语法奇特,有些流畅自然,各有特点,但都差不多。不用担心语法记不住,有个大致的理解就行了,需要的时候再回来翻,只需要关注语言之间的区分,以及该语言独有的一些特性即可
参考文档:
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)。
整数 是 没有小数部分的数字,具体有如下几种类型:
表示方式为 有无符号 + 类型大小(位数),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模式匹配的其他用法:
- 定义 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)); }
- 直接使用 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); }
- 结构体模式匹配:
// 定义坐标点结构体 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];
如果字符串包含汉字,在获取字符串切片时有什么要注意的?
字符串切片的索引位置是按照字节而不是字符。由于汉字使用 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,跟我们上节的动态字符串切片类型一样,因为它也是一种字符串引用,对程序二进制文件中静态分配的字符串数据的引用,也称之为静态字符串切片。
字符串字面量 和 动态字符串切片的异同点?
相同点:
- 都是都是对字符串数据的引用,而不是实际的字符串数据本身
- 都是 UTF-8 编码的字符串
- 两者都可以使用一些相似的字符串操作,如切片、查找、比较等。
不同点:
- 字符串字面量被硬编码到程序二进制文件中,因此在整个程序运行期间有效。而字符串切片取决于引用它的变量或数据结构的生命周期。
- 字符串字面量在编译时已知大小,是固定大小的;字符串切片在运行时确定大小,是动态大小的。
// 字符串字面量 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。
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。
作者:加密鲸拓
版权:此文章版权归 加密鲸拓 所有,如有转载,请注明出处!