Rust集合(AI回答)

数组 和集合

在 JS 或 Python 里,"数组"和"列表"基本是一回事,都是随便往里塞数据的。但在 Rust 这种系统级语言里,严格区分了 "原生数组(Array)" 和 "集合(Collections)"。

我们分两部分讲:第一部分看懂它们底层的核心区别 ,第二部分认识日常最常用的 4 大集合。


一、 核心区别:Rust 数组(Array) vs 集合(以动态数组 Vec 为例)

你可以把 原生数组 想象成 "焊死格数的铁制鸡蛋托(固定 6 格)" ,把 集合(Vec) 想象成 "能无限伸缩的橡胶麻袋"。

|--------------|-------------------------------------------------------|---------------------------------------------|
| 对比维度 | 原生数组(Array) | 集合之动态数组(Vec<T>) |
| 创建写法 | let a = 1, 2, 3; (不带 vec!) | let v = vec!1, 2, 3; (带 vec! 宏) |
| 类型名称 | i32; 3 (类型里焊死了长度 3) | Vec<i32> (类型里没有长度限制) |
| 数据存在哪? | 栈内存(Stack) ------ 极快,零分配开销 | 堆内存(Heap) ------ 动态申请空间 |
| 能不能增删元素? | ❌ 绝对不能! 长度在编译期就锁死了,没有 .push() 方法 | ✅ 随时增删! 可以 .push() 追加、.pop() 弹出,装不下自动扩容 |
| 适用场景 | 长度永远不变的死数据(如一年 12 个月、RGB 三原色 u8; 3、坐标 f64; 2) | 日常开发 95% 的列表场景(从数据库查出的用户列表、动态增减的数据) |

💡 一个让你秒懂原生数组有多"死板"的例子 :

在 Rust 里,i32; 3(装 3 个数的数组)和 i32; 4(装 4 个数的数组)是两个完全不同的类型! 如果一个函数写死了接收 i32; 3,你传一个 4 个数的数组进去,编译直接报错!

(这也是为什么我们在前面写 largest 函数时,参数写的是切片借用 &T------因为 &T 是一块"通用透视窗",无论是栈上的定长数组 1, 2, 3,还是堆上的 vec!1, 2, 3,都能借用成 &T 传进去!)


二、 Rust 最核心的"四大集合"(全在堆内存上,可动态伸缩)

除了只能装同一种元素的数组外,Rust 标准库提供了各种数据结构来组织数据,统称为 集合(Collections)。日常出镜率最高的有 4 个:

1. Vec<T>(Vector 动态数组 ------ 使用率 80% 以上)

相当于 Java 的 ArrayList、JS 的 \[\]、Python 的 list。直接用无需 use 导入:

Rust

复制代码
fn main() {
    let mut nums = vec![10, 20]; // 创建
    nums.push(30);               // 尾部追加 -> [10, 20, 30]
    nums.pop();                  // 弹出最后一个 -> [10, 20]

    // 读取元素的两种方式(复习你之前学的 Option!):
    let a = nums[0];      // 方式 1:直接下标拿(如果越界,程序当场崩溃 panic!)
    let b = nums.get(99); // 方式 2:安全拿,返回 Option 盒子(越界只返回 None,绝不崩溃)
}
2. HashMap<K, V>(哈希表 / 键值对字典 ------ 查数据极快)

相当于 Java 的 HashMap、JS 的 Map/Object、Python 的 dict。

(注意:HashMap 比较大,默认没自动导入,用之前要先写一行 use std::collections::HashMap;)

Rust

复制代码
use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    // 1. 插入键值对 (Key -> Value)
    scores.insert(String::from("张三"), 95);
    scores.insert(String::from("李四"), 88);

    // 2. 根据 Key 查找(同样返回你熟悉的 Option<&i32> 盒子!)
    if let Some(&score) = scores.get("张三") {
        println!("张三考了: {} 分", score);
    }

    // 3. 遍历字典
    for (name, score) in &scores {
        println!("{}: {}", name, score);
    }
}
3. HashSet<T>(哈希集合 ------ 自动去重 + 极速查重)

相当于 Java 的 HashSet、JS 的 Set、Python 的 set。它内部其实就是一个只存 Key 不存 Value 的 HashMap,里面绝对不会出现重复元素:

Rust

复制代码
use std::collections::HashSet;

fn main() {
    let mut tags = HashSet::new();

    tags.insert("Rust");
    tags.insert("编程");
    tags.insert("Rust"); // 重复塞入 "Rust",会被自动忽略!

    println!("共有 {} 个标签", tags.len()); // 输出:2

    // 极速判断某个元素在不在里面
    if tags.contains("Rust") {
        println!("包含 Rust 标签!");
    }
}
4. String(动态字符串 ------ 特殊的字节集合)

没错,你一直用的 String 在官方分类里其实也是集合家族 的一员!

因为 String 的底层源码打开一看,其实就是包了一层壳的 Vec<u8>(一个装满 UTF-8 编码字节的动态数组),所以它同样支持 .push_str() 动态扩容、.pop() 删字符等集合操作。


三、 总结:平时写代码怎么选?

  1. 排成一排的数据,需要加减元素

    复制代码
    →→

    闭眼用 Vec<T> (vec!...)。

  2. 个数永远固定死的小数据(如 3D 坐标 x, y, z、固定缓冲区)

    复制代码
    →→

    用原生数组 T; N 。

  3. 需要通过名字/ID 快速查对应的数据(Key-Value)

    复制代码
    →→

    用 HashMap<K, V> 。

  4. 需要去重,或者频繁判断"某东西在不在黑名单/白名单里"

    复制代码
    →→

    用 HashSet<T> 。

  5. 写函数参数想同时兼容 Vec 和原生数组

    复制代码
    →→

    参数类型写成切片引用 &T 。

Rust字符串介绍

在很多语言里,字符串只有一种类型(比如 String);但在 Rust 里,你经常看到 String 和 &str 两个东西成双成对出现。

其实,只要把你上一题刚学的 Vec<T>(动态数组)与 &T(切片透视窗) 套过来,你一秒钟就能看穿 Rust 字符串的本质!

我们分 4 个核心部分 彻底讲透 Rust 字符串:


一、 核心双胞胎:String vs &str 到底什么关系?

在底层,Rust 的字符串本质上就是一串 UTF-8 编码的字节数组。所以它们的关系和上一题的数组完全一一对应:

|--------------|-------------------------------|--------------------------------|
| 对比维度 | String(动态字符串本体) | &str(字符串切片 / 只读视图) |
| 对应上一题的谁? | 相当于 Vec<u8>(橡胶麻袋) | 相当于 &u8(只读透视镜/借条) |
| 有没有所有权? | ✅ 有所有权(自己花钱在堆内存买的房) | ❌ 没有所有权(只是一张指向别人文字的借条) |
| 能不能修改? | ✅ 可以修改(.push_str() 追加、删改) | ❌ 绝对只读(只能看,不能改) |
| 常见长相 | String::from("你好") | 代码里直接写的双引号 "你好",或者 &s0..3 |

💡 它们俩怎么互相转换?
  • 从 &str 变成 String(花钱复印上堆内存):

    • "hello".to_string() 或 String::from("hello") 或 "hello".into()(三者完全等价!)
  • 从 String 变成 &str(零成本借看一眼):

    • 直接在前面加个 & 即可(比如 &my_string)!
💡 平时写代码时,到底该选哪个?

记住两条无脑黄金法则:

  1. 定义结构体 struct 的字段时

    复制代码
    →→

    选 String (比如 struct User { name: String },因为对象得自己拥有自己的名字)。

  2. 写函数的只读参数时

    复制代码
    →→

    一律写 &str (比如 fn greet(name: &str))!因为写 &str 既能直接传 "张三" 字面量,又能传 &my_string,两边通吃!


二、 避坑指南:为什么 Rust 严禁写 s0 拿第 0 个字符?

在 JS 或 Python 里,你想拿字符串 "你好" 的第一个字,直接写 s0 就能拿到 '你'。

但在 Rust 里,如果你敢写 let c = s0;,编译器会当场报错拒绝编译!

为什么?(UTF-8 编码的真相)

因为 Rust 的字符串在底层存的是 UTF-8 字节,而不同文字占用的字节长度是不一样的:

  • 英文 'a':只占 1 个字节

  • 汉字 '你':占 3 个字节!

  • 表情符号 '🦀':占 4 个字节!

Text

复制代码
字符串 "a你" 在内存里的真实字节排布(共 4 个字节):
下标:   [0]      [1]    [2]    [3]
内容:   'a'   |────── '你' ──────|
  • 后果 1 :如果你写 s1,拿到的根本不是 '你',而是汉字 '你' 的前三分之一残肢(一个毫无意义的乱码字节)!

  • 后果 2 :.len() 返回的是字节数 ,而不是字数!"你好".len() 算出来是 6(2 个汉字

    复制代码
    ××

    3 字节),而不是 2!

  • 后果 3(切片警告):你之前截图里的 &s0..3 切的是英文 "runoob"(前 3 个字节正好是 "run")。如果 s 是中文 "你好",写 &s0..3 正好切出 3 个字节的 "你";但如果你写 &s0..2,因为把一个汉字劈成了两半,程序会在运行时直接崩溃报错!

那我想安全地遍历每一个"字",该怎么写?

用 .chars() 方法!它会自动帮你把字节拼成一个个完整的字符(char):

Rust

复制代码
fn main() {
    let s = "a你好🦀";

    println!("占用的字节数: {}", s.len());          // 输出:11 (1 + 3 + 3 + 4)
    println!("真实的字符数: {}", s.chars().count()); // 输出:4  (共 4 个字)

    // 安全遍历每一个字符
    for c in s.chars() {
        println!("字符: {}", c); // 依次完美输出:a、你、好、🦀
    }

    // 如果非要拿"第 1 个字"(下标为 1),用 .chars().nth(1),返回你熟悉的 Option 盒子!
    if let Some(word) = s.chars().nth(1) {
        println!("第 1 个字是: {}", word); // 输出:你
    }
}

三、 日常最常用的字符串操作(拼接、修改、查找)

1. 拼接字符串:强烈推荐用 format! 宏(别用 + 号!)

在 Rust 里虽然可以用 + 拼接字符串(let s3 = s1 + &s2;),但 + 号有个很烦人的副作用:它会把第一个变量 s1 的所有权吃掉(导致后面不能再用 s1)。

而在实际开发中,大家 99% 都在用 format! 宏 (用法跟 println! 一模一样,只是它不打印到屏幕,而是拼好返回一个新 String,且绝对不会夺走任何原变量的所有权!):

Rust

复制代码
let s1 = String::from("Hello");
let s2 = String::from("Rust");

// ✨ 最优雅的拼接方式:format! 宏
let result = format!("{} - {}!", s1, s2); 

println!("{}", result); // 输出:Hello - Rust!
println!("s1 和 s2 依然活着: {}, {}", s1, s2); // ✅ 原变量完好无损!
2. 修改字符串(追加)

Rust

复制代码
let mut s = String::from("你好");
s.push_str(",世界"); // push_str 追加一串字符串(双引号)
s.push('!');         // push 追加单个字符 char(注意是单引号!)
// 此时 s 变成了 "你好,世界!"
3. 常用查询与处理方法(和 JS / Python 几乎一模一样)

Rust

复制代码
let text = "  hello,rust,world  ";

let clean = text.trim();                      // 去掉首尾空格 -> "hello,rust,world"
let has_rust = clean.contains("rust");        // 是否包含子串 -> true
let new_text = clean.replace("rust", "RUST"); // 替换文字 -> "hello,RUST,world"

// 按逗号分割并收集成 Vec 数组(复习你前面学的 .collect() !)
let parts: Vec<&str> = clean.split(',').collect(); 
// parts 结果为:["hello", "rust", "world"]

总结口诀

  1. 身份区分:String 是堆上的房东(可改、有所有权);&str 是只读租客/切片(不可改、适合做函数参数)。

  2. 拼接首选 :无脑用 format!("{}{}", a, b),省心又不丢所有权。

  3. 中文注意 :.len() 算的是字节数(1 个汉字 = 3 字节);想按字操作,先调 .chars()!

相关推荐
曦樂~22 分钟前
【Web Development】03JavaScript 基础和事件
开发语言·前端·javascript
我不会起名字32225 分钟前
req.Seqs[len-1]:空切片防御缺口与 RPC 二次校验
后端·mongodb·go·即时通讯·im·故障排查
郝学胜-神的一滴27 分钟前
LangChain Skill 实现(渐进披露 On-Demand Skill,LangChain 官方 Deep Agent Skill 模式)
开发语言·python·langchain·numpy
脚本自动化开发1 小时前
京东PLUS开卡红包刷50元的方法
后端
程序员Query1 小时前
day01-认识Go语言 + golang环境的安装
开发语言·后端·golang
码事漫谈9 小时前
黄仁勋被裁第一天,就收到了橄榄枝
后端
金銀銅鐵10 小时前
带您看懂字节码指令(二):空循环里有什么
java·后端·ai编程
§ꦿCFོ༉11 小时前
嵌入式八股文—C语言入门学习(四)
c语言·开发语言·学习
投图匠11 小时前
十种不同风格的 CTP接口 框架
开发语言·python