【Rust通用集合类型】Rust向量Vector、String、HashMap原理解析与应用实战
✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Rust开发,Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。所属的专栏:Rust语言通关之路
景天的主页:景天科技苑
文章目录
- 通用集合类型
- 1、vector
- 1.1 创建 Vector
- 1.2 添加/更新元素
- 1.3 丢弃 vector 时也会丢弃其所有元素
- 1.4 访问vector元素
- 1.5 无效引用
- 1.6 遍历 vector 中的元素
- 1.7 使用枚举来储存多种类型
- 1.8 vector容量管理
- 1.9 vector 与其他集合转换
- 1.10 vector 常用方法
- 2、Rust字符串
- 2.1 什么是字符串?
- 2.2 新建字符串
- 1)String::new()创建
- 2)通过字符串字面量 to_string()创建
- 3)使用 String::from 创建 String
- 4)使用 to_owned 方法
- 5)r#原字符串
- 2.3 字符串操作
- 1)追加内容
- 2)连接字符串
- 3)字符串长度
- 4)遍历字符串
- 2.4 字符串索引和切片
- 2.5 字符串常用方法
- 1)检查方法
- 2)转换方法
- 3)分割和拼接
- 4)替换和修剪
- 2.6 字符串与其它类型的转换
- 1)数字与字符串
- 2)路径与字符串
- 2.7 字符串格式化
- 1)使用 format! 宏
- 2)使用 println! 宏
- 2.8 处理 UTF-8 字符串
- 3、 hashmap 储存键值对
- 3.1 HashMap 简介
- 3.2 创建 HashMap 的多种方式
- 1)使用 new() 创建
- 2)使用 with_capacity() 预分配空间
- 3)从元组向量创建
- 4)从数组创建创建
- 5)使用迭代器创建
- 3.3 插入和更新值
- 1)基本插入
- 2)更新已有值
- 3)只在键不存在时插入
- 4)根据旧值更新一个值
- 3.4 访问值
- 1)使用 get 方法
- 2)使用 get_mut 获取可变引用
- 3)遍历hashmap
- 3.5 删除操作
- 1)使用 remove 删除
- 2)使用 remove_entry 删除并返回键值对
- 3.6 哈希算法选择
- 3.7 HashMap的其他一些常见用法
- 1)获取长度 - len()
- 2)检查是否为空 - is_empty()
- 3)清空 HashMap - clear()
- 4)获取键或值的集合
- 5)检查键是否存在
通用集合类型
Rust 标准库中包含一系列被称为 集合(collections)的非常有用的数据结构。
大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。
不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知并且可以随着程序的运行增长或缩小。
每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终成长的技能。
在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合:
vector: 允许我们一个挨着一个地储存一系列数量可变的值
字符串(string):是一个字符的集合。我们之前见过 String 类型,不过在本文我们将深入了解。
哈希 map(hash map):允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。
1、vector
Vector (通常写作 Vec<T>
) 是 Rust 标准库中最常用的集合类型之一,它是一个可增长的、堆分配的数组类型。
vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。
vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用。
1.1 创建 Vector
使用 Vec::new() 创建空 Vector
let mut v: Vec<i32> = Vec::new(); // 需要类型注解,因为还没有插入元素,Rust 并不知道我们想要储存什么类型的元素。
这是一个非常重要的点。vector 是用泛型实现的。你需要知道的就是 Vec 是一个由标准库提供的类型,它可以存放任何类型,而当 Vec 存放某个特定类型时,那个类型位于
尖括号中。这里我们告诉 Rust v 这个 Vec 将存放 i32 类型的元素。
在更实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。
更常见的做法是使用初始值来创建一个 Vec ,而且为了方便 Rust 提供了 vec! 宏。这个宏会根据我们提供的值来创建一个新的 Vec 。
let v = vec![1, 2, 3]; // 自动推断为 Vec<i32>
let v = vec![0; 5]; // 创建包含5个0的vector: [0, 0, 0, 0, 0]
1.2 添加/更新元素
添加元素
对于新建一个 vector 并向其增加元素,可以使用 push 方法
let mut v = Vec::new();
v.push(1); // 添加元素到末尾
v.push(2);
v.push(3);
更新 Vector
除了 push 方法外,还有:
insert: 在某个位置插入元素
remove: 移出指定索引的元素
pop: 移出并返回最后一个元素
let mut v = vec![1, 2, 3];
v.insert(1, 4); // 在索引1处插入4: [1, 4, 2, 3]
v.remove(2); // 移除索引2处的元素: [1, 4, 3]
v.pop(); // 移除并返回最后一个元素: Some(3)
println!("v: {:?}", v);
1.3 丢弃 vector 时也会丢弃其所有元素
类似于任何其他的 struct ,vector 在其离开作用域时会被释放
{let v = vec![1, 2, 3, 4];// do stuff with v
} // <- v goes out of scope and is freed here
当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。
这可能看起来非常直观,不过一旦开始使用 vector 元素的引用,情况就变得有些复杂了。
1.4 访问vector元素
有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。
1)使用索引语法
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2]; // 索引从0开始
println!("第三个元素是 {}", third);
2)使用 get 方法
通过match结合 get(索引)获取
当 get 方法被传递了一个数组外的索引时,它不会 panic 而是返回 None
当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。
//使用get方法获取vector元素
let v = vec![1, 2, 3, 4, 5];
let five = v.get(4); // 索引从0开始
println!("第5个元素是 {:?}", five);
得到的是个Option
当下标越界时,不会报错,返回None
结合match,可以获取Option值
let v = vec![1, 2, 3, 4, 5];
match v.get(2) {
Some(third) => println!("第三个元素是 {}", third),
None => println!("没有第三个元素"),
}
索引获取与get方法获取的区别
索引语法在越界时会 panic,而 get 方法返回 Option。
使用索引获取,下标越界报错
get方法越界返回None
1.5 无效引用
一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。
当我们获取了 vector 的第一个元素的不可变引用,并尝试在 vector 末尾增加一个元素的时候,这是行不通的
//vector无效引用
let v = vec![1, 2, 3];
let first = &v[0]; // 创建一个对第一个元素的引用
v.push(4); // 错误:不能在有引用的情况下修改vector
println!("first: {}", first); // 使用引用
为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于vector 的工作方式。
在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。
这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。
1.6 遍历 vector 中的元素
如果想要依次访问 vector 中的每一个元素,我们可以遍历其所有的元素而无需通过索引一次一个的访问。
遍历vector分为不可变引用遍历和可变引用遍历
不可变遍历
let v = vec![100, 32, 57];
for i in &v {println!("{}", i);
}
可变遍历
let mut v = vec![100, 32, 57];
for i in &mut v {*i += 50; // 使用 * 解引用并修改
}
println!("{:?}", v);
为了修改可变引用所指向的值,在使用 += 运算符之前必须使用解引用运算符( * )获取 i 中的值。
1.7 使用枚举来储存多种类型
我们提到 vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。
幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!
这样最终就能够储存不同类型的值了
#[derive(Debug)]
enum SpreadsheetCell {Int(i32),Float(f64),Text(String),
}let row = vec![SpreadsheetCell::Int(3),SpreadsheetCell::Text(String::from("blue")),SpreadsheetCell::Float(10.12),
];
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。
第二个好处是可以准确的知道这个 vector 中允许什么类型。
如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。
使用枚举外加 match 意味着 Rust 能在编译时就保证总是会处理所有可能的情况。
如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。但是,你可以使用trait 对象。
1.8 vector容量管理
//vector容量管理
let mut v = Vec::with_capacity(10); // 预分配容量
v.push(1);
println!("长度: {}, 容量: {}", v.len(), v.capacity());v.shrink_to_fit(); // 减少容量到刚好容纳元素
println!("长度: {}, 容量: {}", v.len(), v.capacity());
1.9 vector 与其他集合转换
//vector与其他集合转换
// 从数组创建
let arr = [1, 2, 3];
// 转换为vector
let v = arr.to_vec();
println!("v: {:?}", v);
// vector转换为数组
// let arr: [i32; 3] = v.try_into().unwrap();
// println!("arr: {:?}", arr);// vector转换为切片
let slice = &v;
println!("slice: {:?}", slice);
// vector转换为迭代器
let iter = v.into_iter();
for i in iter {println!("iter: {}", i);
}
1.10 vector 常用方法
len(): 获取长度
is_empty(): 检查是否为空
contains(&value): 检查是否包含某个值
sort(): 排序
reverse(): 反转
split_off(at): 分割 vector
append(&mut other): 合并另一个 vector
dedup(): 移除连续重复元素,一般是先排序,后去重
contains使用示例:
let v = vec!["jingtian", "zhangsanfeng", "zhangwuji"];
let a = "jingtian";
if v.contains(&a) {println!("v contains {}", a);
} else {println!("v does not contain {}", a);
}
2、Rust字符串
在这之前,我们已经多次使用到了字符串了,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,
这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结
构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。
字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方
法提供了实用的功能。在这一部分,我们会讲到 String 中那些任何集合类型都有的操作,比如创建、更新和读取。也
会讨论 String 与其他集合不一样的地方,例如索引 String 是很复杂的,由于人和计算机理解 String 数据方式的不同。
2.1 什么是字符串?
Rust 的核心语言中只有一种字符串类型: str ,字符串 slice,它通常以被借用的形式出现, &str 。
它们是一些储存在别处的UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。
称作 String 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。
当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 String 和字符串 slice &str 类型,而不仅仅是其中之一。
虽然本部分内容大多是关于 String 的,不过这两个类型在 Rust 标准库中都被广泛使用, String 和字符串 slice 都是 UTF-8 编码的。
Rust 标准库中还包含一系列其他字符串类型,比如 OsString 、 OsStr 、 CString 和 CStr 。相关库 crate 甚至会提供更多储存字符串数据的选择。
与 *String / *Str 的命名类似,它们通常也提供有所有权和可借用的变体,就比如说String / &str 。
这些字符串类型在储存的编码或内存表现形式上可能有所不同。
String - 可增长、可修改、拥有所有权的 UTF-8 编码字符串
&str - 固定大小的字符串切片,通常是对 String 的借用或字符串字面量
为什么 Rust 需要两种字符串类型?
Rust 的这种设计主要是为了:
性能优化:&str 是轻量级的,不需要堆分配
所有权明确:String 拥有数据,&str 只是借用
灵活性:可以在需要时选择使用拥有所有权的或借用的字符串
2.2 新建字符串
很多 Vec 可用的操作在 String 中同样可用,
1)String::new()创建
从以 new 函数创建字符串开始,
let mut s = String::new();
新建一个空的 String
这新建了一个叫做 s 的空的字符串,接着我们可以向其中装载数据。
通常字符串会有初始数据,因为我们希望一开始就有这个字符串。
为此,可以使用 to_string 方法,它能用于任何实现了 Display trait 的类型,字符串字面值就可以。
2)通过字符串字面量 to_string()创建
使用 to_string 方法从字符串字面值创建 String
let data = "initial contents";
let s = data.to_string();
// the method also works on a literal directly:
let s = "initial contents".to_string();
3)使用 String::from 创建 String
也可以使用 String::from 函数来从字符串字面值创建 String
let s = String::from("initial contents");
println!("s: {}", s);
4)使用 to_owned 方法
通过字符串字面量的to_owned()方法返回字符串String
let s3 = "initial contents".to_owned();
println!("s3: {}", s3);
5)r#原字符串
Rust的字符串字面量使用反斜杠\作为转义字符,比如\n表示换行,\t表示制表符等。但是,如果你只是想在字符串中包含一个普通的反斜杠字符,你需要用两个反斜杠\来表示。
如果字符串中包含\,直接这样表示是会报错的
//原字符串
let s = String::from("\hello");
println!("原字符串: {}", s);
如果想要在字符串中包含\,可以使用转义符
//原字符串
let s = String::from("\\hello");
println!("原字符串: {}", s);
也可以使用r#,原字符串
使用了r#“…”#来定义了一个原始字符串字面量,它允许字符串内部包含任意的字符,包括换行符、tab符号和引号等,而不需要使用转义字符。
//原字符串
let s = String::from(r#"\hello"#);
println!("原字符串: {}", s);
2.3 字符串操作
1)追加内容
push_str() 追加字符串
push() 追加单个字符
push_str的参数都是&str类型,当然&String可以自动转换为&str类型
push的参数是char类型
let mut s = String::from("hello");
// 追加字符串
s.push_str(" world");
// 追加单个字符
s.push('!');
println!("{}", s); // 输出: hello world!
2)连接字符串
使用 + 运算符或 format! 宏连接字符串
使用+连接
通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用 + 运算符
使用 + 运算符将两个 String 值合并到一个新的 String 值中
// 使用 + 运算符
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = s1 + &s2; // 注意 s1 的所有权被移动,s1的所有权已经给了s3了。此后s1不再有效
println!("{}", s3); // 输出: hello world!
执行完这些代码之后字符串 s3 将会包含 Hello, world! 。 s1 在相加后不再有效的原因,和使用 s2 的引用的原因与使用 + 运算符时调用的方法签名有关,这个函数签名看起来像这样:
fn add(self, s: &str) -> String {
字符串运算+,相当于执行了这个add函数,字符串相加第一个必须是String,+后面的都是字符串引用
这并不是标准库中实际的签名;标准库中的 add 使用泛型定义。这里我们看到的 add 的签名使用具体类型代替了泛型,
这也正是当使用 String 值调用这个方法会发生的。后面我们会讨论泛型。这个签名提供了理解 + 运算那微妙部分的线索。
首先, s2 使用了 & ,意味着我们使用第二个字符串的 引用 与第一个字符串相加。
这是因为 add 函数的 s 参数:只能将 &str 和 String 相加,不能将两个 String 值相加。
不过等一下——正如 add 的第二个参数所指定的, &s2 的类型是 &String 而不是 &str 。
那么为示例 还能编译呢?
之所以能够在 add 调用中使用 &s2 是因为 &String 可以被 强转(coerced)成 &str ——当 add 函数被调用时,
Rust 使用了一个被称为 解引用强制多态(deref coercion)的技术,你可以将其理解为它把 &s2 变成了 &s2[…] 。
后面我们会更深入的讨论解引用强制多态。因为 add 没有获取参数的所有权,所以 s2 在这个操作后仍然是有效的 String 。
其次,可以发现签名中 add 获取了 self 的所有权,因为 self 没有 使用 & 。这意味着上面例子中的 s1 的所有权将
被移动到 add 调用中,之后就不再有效。所以虽然 let s3 = s1 + &s2; 看起来就像它会复制两个字符串并创建一个新的
字符串,而实际上这个语句会获取 s1 的所有权,附加上从 s2 中拷贝的内容,并返回结果的所有权。
换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。
使用 format! 宏
// 使用 format! 宏
let s1 = String::from("hello");
let s2 = String::from("world");
let s3 = format!("{} {}!", s1, s2);
println!("{}", s3); // 输出: hello world!
println!("{}", s1); // s1 仍然有效,因为 format! 宏不会移动 s1 的所有权
println!("{}", s2); // s2 仍然有效,因为 format! 宏不会移动 s2 的所有权
format! 与 println! 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String 。
这个版本就好理解的多,并且不会获取任何参数的所有权。
高效连接多个字符串
fn concatenate_strings(strings: &[&str]) -> String {// 预先计算总长度以避免多次分配let total_length = strings.iter().map(|s| s.len()).sum();let mut result = String::with_capacity(total_length);for s in strings {result.push_str(s);}result
}fn main() {let parts = ["Rust", " is", " a", " systems", " programming", " language"];let combined = concatenate_strings(&parts);println!("{}", combined); // 输出: Rust is a systems programming language
}
3)字符串长度
let s = "hello";
println!("{}", s.len()); // 输出: 5let s = "你好";
println!("{}", s.len()); // 输出: 6 (UTF-8编码)
根据utf-8编码中每个字符占的字节来计算长度,每个汉字占3个字节
4)遍历字符串
遍历字符串可以从两个方面遍历,字符方式遍历和字节方式遍历
- chars方法遍历,返回每个字符
// 按字符遍历
for c in "नमस्ते".chars() {println!("{}", c);
}
- 按字节遍历
bytes 方法返回每一个原始字节// 按字节遍历for b in "नमस्ते".bytes() {println!("{}", b);}
2.4 字符串索引和切片
在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。
然而在 Rust 中,如果我们尝试使用索引语法访问 String 的一部分,会出现一个错误。
Rust 不允许直接使用索引访问字符串中的字符,因为字符串是 UTF-8 编码的,不同语言字符可能占用 字节个数不确定。
let s1 = String::from("hello");
let h = s1[0];
错误和提示说明了全部问题:Rust 的字符串不支持索引。
可以使用字符串切片来索引
slice 允许你引用集合中一段连续的元素序列
针对非ASCII码中的字符,一定要注意边界,才能索引,否则也会报错
let s = "hello";
// 切片 (字节位置)
let slice = &s[0..2]; // "he"
// 下面的代码会 panic,因为不是字符边界
// let slice = &s[0..3]; // 可能 panic 如果 3 不是字符边界
安全使用字符串切片提取子字符串
由于不同的语言,单个字符所占的字节不同,所以用户输入的索引可能在字符中间,这样切片索引就会失败。
可以根据用户输入的是否字符边界,来判断输入的索引是否合法
is_char_boundary(index) 可以判断索引是否边界
fn extract_substring(s: &str, start: usize, end: usize) -> Option<&str> {// 首先检查是否是字符边界if !s.is_char_boundary(start) || !s.is_char_boundary(end) {return None;}// 然后检查范围是否有效if start > end || end > s.len() {return None;}Some(&s[start..end])
}fn main() {let s = "Hello, 世界!";match extract_substring(s, 7, 13) {Some(sub) => println!("Substring: {}", sub), // 输出: 世界None => println!("Invalid substring range"),}
}
2.5 字符串常用方法
1)检查方法
let s = String::from("hello");// 检查是否为空
println!("{}", s.is_empty()); // false// 检查是否包含子串
println!("{}", s.contains("ell")); // true// 检查是否以某字符串开头/结尾
println!("{}", s.starts_with("he")); // true
println!("{}", s.ends_with("lo")); // true
2)转换方法
let s = String::from("Hello World");// 转换为大写/小写
println!("{}", s.to_lowercase()); // "hello world"
println!("{}", s.to_uppercase()); // "HELLO WORLD"// 转换为字符串切片
let slice: &str = &s;// 转换为字节数组
let bytes = s.as_bytes();
3)分割和拼接
let s = "hello world";// 分割字符串,以空格为分隔符
for word in s.split_whitespace() {println!("{}", word);
}// 拼接字符串,将字符串数组连接成一个字符串。join的参数是合并后的单词分隔符
let words = ["hello", "world"];
let joined = words.join(" ");
println!("{}", joined); // "hello world"
4)替换和修剪
let s = " hello world ";// 修剪空白字符,去除两边的空白符
println!("{}", s.trim()); // "hello world"// 替换
let replaced = s.replace("world", "Rust");
println!("{}", replaced); // " hello Rust "
2.6 字符串与其它类型的转换
1)数字与字符串
// 数字转字符串
let num = 42;
let num_str = num.to_string();
println!("num_str1: {}", num_str);
// 或使用 format! 宏
let num_str = format!("{}", num);
println!("num_str2: {}", num_str);// 字符串转数字
let num_str = "42";
let num: i32 = num_str.parse().unwrap();
println!("num: {}", num);// 或使用 turbofish 语法
let num = num_str.parse::<i32>().unwrap();
println!("num: {}", num);
2)路径与字符串
使用到路径Path库
use std::path::Path;let path = Path::new("/tmp/foo.txt");
let path_str = path.to_str().unwrap();
2.7 字符串格式化
1)使用 format! 宏
let name = "Alice";
let age = 30;
let s = format!("{} is {} years old", name, age);
println!("{}", s);
2)使用 println! 宏
println!("{} is {} years old", name, age);// 格式化数字
let pi = 3.1415926;
println!("{:.2}", pi); // 3.14
2.8 处理 UTF-8 字符串
Rust 字符串严格使用 UTF-8 编码,这带来了一些特殊考虑,字符边界问题
s.chars().count() 获取字符个数
s.chars().nth(n).unwrap() 获取第n个字符let s = "नमस्ते";// 获取字符数量 (不是字节数)
println!("{}", s.chars().count()); // 6// 获取第 n 个字符
let third_char = s.chars().nth(2).unwrap();
println!("{}", third_char); // 'म'
3、 hashmap 储存键值对
3.1 HashMap 简介
HashMap 是 Rust 标准库中提供的键值对集合类型,基于哈希表实现。它提供了高效的数据查找、插入和删除操作。
它存储键值对并提供了平均时间复杂度为 O(1) 的查找、插入和删除操作。
HashMap<K, V> 类型储存了一个键类型 K 对应一个值类型 V 的映射。
它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。
HashMap<K, V> 存储的是 K 类型的键和 V 类型的值的映射关系。
它要求键类型 K 实现了 Eq 和 Hash trait,而值类型 V 可以是任意类型。
例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。
给出一个队名,就能得到它们的得分。
HashMap特性
- 快速查找:平均 O(1) 的时间复杂度,适用于需要频繁插入、删除和查找键值对的场景。
- 无序:键值对的顺序不保证,哈希表的存储顺序是由哈希值决定的。
- 可以动态调整大小,以应对负载因子变化。
HAashMap的桶与槽
在哈希表(例如 Rust 的 HashMap)的实现中,桶(bucket)是用来存储键值对的容器或位置。
每个桶存储着哈希表中一个特定的“槽”中的元素,多个元素可能会被存放到同一个桶中。
这种设计的目的是为了优化哈希表的性能,使得查找、插入和删除操作能够在常数时间复杂度(O(1))内完成。
如何理解桶?
哈希函数:当你插入一个键值对时,哈希表首先会通过一个哈希函数计算出该键的哈希值。哈希值是一个整数,表示该键在哈希表中的位置。
桶的数量:哈希表通常将桶的数量设置为一个固定的大小,这个大小可能是基于哈希表的当前大小动态扩展的。哈希表会根据哈希值将键映射到相应的桶。
桶的作用:每个桶是一个容器,桶中可以存放一个或多个键值对。哈希表通过哈希值来确定要查找哪个桶,进而查找该桶中的键值对。
桶冲突(碰撞):当多个键的哈希值相同,或者它们的哈希值被映射到相同的桶时,就会发生哈希碰撞。
为了处理这种情况,哈希表通常会使用一种方法来将多个元素存储在同一个桶中,常见的解决方案有:
链式法(Chaining):每个桶实际上是一个链表(或者其他数据结构),多个键值对就可以在同一个桶中按链表的形式存储。
开放寻址法(Open Addressing):当发生碰撞时,哈希表会寻找另一个空桶来存储这个元素。
多次哈希:如果下标位置已被占用,就用另外一个hash函数计算新的下标位置。当然理论上来讲,第二个hash函数算出的下标位置仍然可能已经被占用。
工程实践上,前2种方式比较容易实现。比如Java中的HashMap是用链地址法处理哈希冲突;Rust中的HashMap是用开放寻址法中的二次寻址方式处理哈希冲突。
为什么桶重要?
桶的设计使得哈希表能够有效地组织数据并在平均常数时间内执行操作。如果没有桶,哈希表可能会退化成一个线性查找的结构,失去哈希表的效率优势。
在 Rust 的 HashMap 中,桶的数量和哈希函数一起决定了哈希表的性能。当碰撞较多时,哈希表可能会进行扩展,增加桶的数量,以保持较好的性能。
HashMap是无序的,如果要使用有序的map,可以使用BTreeMap
use std::collections::BTreeMap;
-实现
基于平衡二叉树(通常是红黑树)实现,确保键按顺序排列。
特性
- 有序:键值对按键的排序顺序存储(默认是升序),可以进行范围查询等操作。
- 查找性能较差:查找、插入、删除的时间复杂度是 O(log n),比哈希表稍慢。
- 适用于需要键有顺序的场景,如按键排序或按范围查询。
3.2 创建 HashMap 的多种方式
创建hashmap的时候,首先需要将hashmap的库导入进来
use std::collections::HashMap;
1)使用 new() 创建
指定hashmap的键值类型:HashMap<键的类型, 值的类型>
当然,也可以根据插入的键值进行自动推导
let mut map: HashMap<String, i32> = HashMap::new();
2)使用 with_capacity() 预分配空间
//预分配空间
let mut map = HashMap::with_capacity(100); // 预先分配空间,减少扩容次数
for i in 0..100 {map.insert(i, i);
}
println!("{:?}", map);
3)从元组向量创建
let tuples = vec![("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<_, _> = tuples.into_iter().collect();
4)从数组创建创建
//从数组创建HashMap
let arr = [("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<_, _> = arr.iter().cloned().collect();
println!("{:?}", map);
5)使用迭代器创建
let keys = vec!["a", "b", "c"];
let values = vec![1, 2, 3];
let map: HashMap<_, _> = keys.iter().zip(values.iter()).collect();
3.3 插入和更新值
1)基本插入
let mut scores = HashMap::new();
scores.insert("Blue", 10);
scores.insert("Yellow", 50);
println!("{:?}", scores);
2)更新已有值
对于已经存在的键,执行插入操作就是更新已有值
scores.insert("Blue", 25); // 覆盖原来的值10
3)只在键不存在时插入
scores.entry("Blue").or_insert(30); // 不会改变,因为Blue已存在
scores.entry("Red").or_insert(50); // 会插入Red:50
4)根据旧值更新一个值
另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它
例如: 计数一些文本中每一个单词分别出现了多少次。我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词。
如果是第一次看到某个单词,就插入值 0 。
let text = "hello world wonderful world";let mut map = HashMap::new();for word in text.split_whitespace() {//这里使用的是entry().or_insert() 。只有单词第一次出现的时候,才将该键插入到map,后续就不再继续插进去修改,而是通过修改count来增加次数let count = map.entry(word).or_insert(0);//只要单词出现过一次,就将count加上1,从而可以统计单词出现的次数*count += 1;
}println!("{:?}", map); // 输出: {"world": 2, "hello": 1, "wonderful": 1}
通过哈希 map 储存单词和计数来统计出现次数
or_insert 方法事实上会返回这个键的值的一个可变引用( &mut V )。
这里我们将这个可变引用储存在 count 变量中,所以为了赋值必须首先使用星号( * )解引用 count 。
这个可变引用在 for 循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。
3.4 访问值
1)使用 get 方法
get的参数是个引用
返回的是Option<&V>
//访问值
let team_name = "Blue";
let score = scores.get(team_name); // 返回Option<&i32>
println!("{}: {:?}", team_name, score);
//使用match语句处理Option
match score {Some(s) => println!("Score: {}", s),None => println!("Team not found"),
}
可以使用功能if let简化
//访问值
let team_name = "Blue";
let score = scores.get(team_name); // 返回Option<&i32>
println!("{}: {:?}", team_name, score);
//使用match语句处理Option
if let Some(s) = score {println!("Score: {}", s);
} else {println!("Team not found");
}
2)使用 get_mut 获取可变引用
//使用get_mut方法获取可变引用,可用来修改值
if let Some(score) = scores.get_mut("Blue") {*score += 10;
}
可以看到Blue的值加了10
3)遍历hashmap
注意:遍历使用的是hashmap的引用,如果不使用引用,此时的hashmap就被借用,后续就不可以再用这个hashmap了
//遍历HashMap
for (key, value) in &scores {println!("{}: {}", key, value);
}
3.5 删除操作
1)使用 remove 删除
返回被删除的值Option<V>
//删除键值对
let a = scores.remove("Blue"); // 返回被删除的值Option<V>
println!("Removed: {:?}", a);
println!("scores: {:?}", scores);
2)使用 remove_entry 删除并返回键值对
if let Some((team, score)) = scores.remove_entry("Blue") {println!("Removed {} with score {}", team, score);
}
3.6 哈希算法选择
Rust 默认使用加密安全的哈希算法,但有时我们需要更快的哈希算法。
需要先安装包
cargo add twox_hash
use std::collections::HashMap;
use std::hash::BuildHasherDefault;
use twox_hash::XxHash64;type FastHashMap<K, V> = HashMap<K, V, BuildHasherDefault<XxHash64>>;fn main() {let mut map: FastHashMap<&str, i32> = FastHashMap::default();map.insert("foo", 42);
}
使用更换过hash算法的类型创建hashmap
3.7 HashMap的其他一些常见用法
1)获取长度 - len()
println!("Number of teams: {}", scores.len());
2)检查是否为空 - is_empty()
if scores.is_empty() {println!("No teams registered!");
}
3)清空 HashMap - clear()
scores.clear(); // 移除所有键值对
4)获取键或值的集合
let teams: Vec<_> = scores.keys().collect();
let points: Vec<_> = scores.values().collect();
println!("teams: {:?}", teams);
println!("points: {:?}", points);
5)检查键是否存在
//检查键是否存在
let score = scores.get("Yellow"); // 返回Option<&V>
if score.is_some() {println!("Yellow team exists");
} else {println!("Yellow team does not exist");
}//使用contains_key方法检查键是否存在
if scores.contains_key("Yellow") {println!("Yellow team exists");
} else {println!("Yellow team does not exist");
}
相关文章:
【Rust通用集合类型】Rust向量Vector、String、HashMap原理解析与应用实战
✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,…...
Kotlin await等待多个异步任务都完成后才进行下一步操作
Kotlin await等待多个异步任务都完成后才进行下一步操作 import kotlinx.coroutines.*fun main() {runBlocking {val tagA "a"val tagB "b"val a async {worker(tagA)}val b async {worker(tagB)}println("${System.currentTimeMillis()} 等待 $t…...
佛山大旺高新区3650 M5 ERP服务器维修案例
1:机器型号:联想system x3650 m5 2:故障问题:纺织公司由于没有专业的it网管,导致服务器各种爆故障灯,本次处理的是用户反馈最近ERP软件使用过程中经常弹出资源不足的报错。 3:于是预约我们工程…...
利用Python生成Xilinx FPGA ROM IP核 .coe初始化文件
以下是一个 Python 脚本,用于生成 Xilinx IP ROM 的.coe 格式初始化文件,假设ROM 深度为 1024,数据位宽为 32bit,使用随机的 32 位无符号数进行初始化: import random# 定义ROM的深度和数据位宽 rom_depth 1024 data…...
配置电子邮件服务
配置电子邮件服务 一.基于Postfix的邮件发送 1. 安装Postfix yum install postfix -y 再下载一个telnet工具 yum -y install telnet 启动Postfix systemctl start postfix systemctl enable postfix 查看系统中端口是否被Postfix使用 netstat -tnlp | gre…...
WGCAT工单系统发现错误 定时处理工单数据任务错误
一直在用WGCAT工单系统,今天在系统日志里,看到了这个错误提示,不知道是什么原因 2025-04-26 07:05:00.000 [taskScheduler-10] INFO com.wgcloud.task.ScheduledTask - 定时处理工单数据任务开始----------2025-04-26 07:05:00 2025-04-26 …...
软件工程(一):黑盒测试与白盒测试
黑盒测试(Black Box Testing) 定义 黑盒测试是指不关心程序内部实现细节,只关注输入和输出的测试方法。把被测软件当作一个“黑盒子”,只依据功能说明书或需求文档来编写测试用例,验证功能是否正确。 特点 不需要了…...
emqx部署
要修改文件-命名空间-节点选择器 #apiVersion: v1 ##kind: ConfigMap ##metadata: ## name: emqx-config ##data: ## emqx.conf: | ## # --- apiVersion: v1 kind: PersistentVolume metadata:name: emqx-pv spec:capacity:storage: 5GivolumeMode: FilesystemaccessMode…...
【KWDB 创作者计划】_KWDB产品技术解读
文章目录 每日一句正能量一、KWDB简介二、官网信息三、技术亮点解读(一)存储引擎(二)查询引擎(三)分布式架构 四、应用场景五、总结 每日一句正能量 你的心为什么这样分散,使得你放慢了脚步。他…...
C++ 表达式求值优先级、结合律与求值顺序(五十九)
1. 运算符优先级与结合律 优先级(Precedence) 决定未加括号时运算符如何“绑”在一起:5 10 * 20 / 2; // 等同于 5 ((10 * 20) / 2)结合律(Associativity) 决定同级运算符的结合方向: 左结合࿰…...
乐理学习笔记(一)---节拍与音符
节拍 衡量音的长度和节奏的基本单位,以强弱关系按照一定的规律循环进行 拍大腿、拍手 类型 上面的这些不同类型节拍的强弱关系中第一个都是强(起确定性作用,而不是音量最大) 强和弱是决定性的区别,每一个强拍是和弦…...
《系统架构 - Java 企业应用架构中的完整层级划分》
文章目录 Java 企业应用架构中的完整层级划分核心层级(基础架构)业务逻辑层接口层基础设施层辅助层级特殊架构层级现代架构扩展层各层调用关系示例分层原则建议 Java 企业应用架构中的完整层级划分 除了常见的 Controller、Service、DAO 等标准层级外&a…...
Adobe Lightroom Classic v14.3.0.8 一款专业的数字摄影后期处理软件
软件介绍 Adobe Lightroom Classic 2025中文激活版(Adobe桌面照片编辑软件)LRC2025(LR2025本地离线版)是一款桌面照片编辑器和相册管理软件的raw格式编辑软件,支持各种RAW图像相机配置,HDR全景照片&#x…...
SQL 易混易错知识点笔记1(drop,role,%,localhost)
DROP 与 DELETE 的区别 DELETE:删除表中的数据行,属于DML操作,可回滚,可带WHERE条件 DELETE FROM table WHERE condition; -- 删除特定行 DELETE FROM table; -- 删除所有行但保留表结构 DROP:删除整个数据库对象(表、…...
C++23 std::bind_back:一种调用包装器 (P2387R3)
文章目录 引言背景知识旧有的绑定工具C20的std::bind_front std::bind_back的定义和功能定义功能 std::bind_back的使用场景简化回调函数部分应用参数 std::bind_back与其他绑定工具的对比与std::bind的对比与std::bind_front的对比 总结 引言 在C的发展历程中,每一…...
使用多线程快速向Excel中快速插入一万条数据案例
当有大量数据需要存入Excel时,使用传统的单线程完成会有以下这些弊端: 导入速度慢:单线程一次只能处理一个任务,在导入大量数据时,需要逐个将数据写入 Excel。这意味着 CPU 在大部分时间里只能处理一个数据块ÿ…...
RestRequest ,newtonsoft解析
var request new RestRequest(Method.GET); IRestResponse response client.Execute(request); Console.WriteLine(response.Content); //保存token Newtonsoft.Json.Linq.JObject obj3 Newtonsoft.Json.Linq.JObject.Pars…...
vs2022解决 此项目需要MFC库。从visual studio安装程序(单个组件选项卡)为正在使用的任何工具和体系结构安装他们问题
使用visual studio 2022创建MFC 单文档的项目,编译器报错: 严重性 代码 说明 项目 文件 行 禁止显示状态 详细信息 错误 MSB8041 此项目需要 MFC 库。从 Visual Studio 安装程序(单个组件选项卡)为正在使用的任何工具集和体系结构安装它们。 osgEarthMFC…...
面试算法高频08-动态规划-03
练习题 题目描述 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每…...
uniapp做app,使用v-for遍历渲染第二层的时候,打包到手机上渲染不出第二层的数据
1.打包apk要严格注意一点,在data中定义的时候要把第二层定义上, pointspower: [{ jcdbh: 1, cgqbhs:[] }] 不然会出现未定义的情况,直接把二层结构定义上,有利无害 2.渲染…...
Uniapp(vue):生命周期
目录 一、Vue生命周期二、Uniapp中页面的生命周期三、执行顺序比较一、Vue生命周期 setup():是在beforeCreate和created之前运行的,所以可以用setup代替这两个钩子函数。onBeforeMount():已经完成了模板的编译,但是组件还未挂载到DOM上的函数。onMounted():组件挂载到DOM完…...
Git技巧:Git Hook,自动触发,含实战分享
Git技巧:Git Hook,自动触发,含实战分享 最近项目需要1个git合入时触发脚本的功能,使用Git Hook功能实现,总结如下: Git项目在路径:repo\.git\hooks下有很多文件,这些文件就是本地钩…...
DeepSeek创始人梁文峰是个什么样的人?
梁文峰是一位在人工智能领域具有深远影响力的企业家和技术创新者,他的个人经历和成就展现了他作为一位技术天才、创新领袖以及社会责任感强的企业家的多重身份。 从学术背景来看,梁文峰出生于广东湛江吴川,17岁时以高考状元的身份考入浙江大…...
【知识科普】今天聊聊CDN
CDN 技术详解:从原理到配置实践 CDN 技术详解:从原理到配置实践一、CDN 核心定义二、工作原理深度解析1. 请求路由机制2. 缓存分层架构3. 内容更新流程 三、核心功能组件1. 基础设施层2. 软件系统 四、典型配置流程(以Cloudflare为例…...
Axure疑难杂症:利用中继器制作三级下拉菜单(逻辑判断进阶)
亲爱的小伙伴,在您浏览之前,烦请关注一下,在此深表感谢! Axure产品经理精品视频课已登录CSDN可点击学习https://edu.csdn.net/course/detail/40420 课程主题:三级下拉菜单 主要内容:条件筛选时的逻辑判断思维,中继器使用 应用场景:复合条件下的下拉列表制作 案例展…...
C语言----操作符详解(万字详解)
目录 1. 操作符的分类 2. 二进制和进制转换 3. 原码 反码 补码 4. 移位操作符 4.1 左移操作符 >> 4.2 右移操作符 >> 5. 位操作符 5.1 按位与 & 5.2 按位或 | 5.3 按位异或 ^ 5.4 按位取反 ~ 练习 整数存储在内存中二进制中1的个数 练习 二进制位…...
docker本地部署ClipCascade,实现跨设备剪贴板同步
1、什么是 ClipCascade ? ClipCascade 是一款开源的轻量级工具,可以自动同步您的剪贴板在多个设备之间,无需按键。它确保设备之间无缝的剪贴板共享,并以端对端加密优先保护隐私。无论您是在不同工作站之间切换,还是仅…...
Android Compose vs 传统View系统:全面对比与选型指南
Android Compose vs 传统View系统:全面对比与选型指南 一、引言 随着Android Jetpack Compose的正式发布,Android开发迎来了全新的声明式UI框架。本文将全面对比Compose与传统View系统的差异,帮助开发者做出合理的技术选型。 二、核心架构…...
CVE-2024-3431 EyouCMS 反序列化漏洞研究分析
易优内容管理系统(EyouCms) 隶属于海口快推科技有限公司,专注中小型企业信息传播解决方案,利用网络传递信息在一定程度上提高了办事的效率,提高企业的竞争力。EyouCms 是一个自由和开放源码的内容管理系统,它是一个可以独立使用的…...
C# wpf
学习网址:控件的父类们 - WPF中文网 - 从小白到大佬 控件的父类: 由此我们可以得出结论,控件的父类们(准确的说,应该叫父类的父类的父类),至少有如下几个类型: DispatcherObjectDependencyObjectVisualU…...
十一、引用与拷贝函数(References the Copy-Constructor)
十一、引用与拷贝函数(References & the Copy-Constructor) 11.1 指针回顾(Review of pointers) 指针可以保存一个地址。 当你定义一个指针时,必须指定它所指向变量的类型,并且应该初始化它。 示例&a…...
数据结构之顺序表
目录 1.线性表 1.1 线性表的定义:零个或多个数据元素的有限序列。 1.2深度解析 1.3 线性表的抽象数据类型 2.顺序表 2.1 顺序表的定义和存储方式 2.2静态顺序表 2.2.1 静态顺序表的使用 2.2.3 为什么我们要使用typedef呢? 2.2.4 为什么我们要使…...
【计算机视觉】三种图像质量评价指标详解:PSNR、SSIM与SAM
图像质量评价指标详解:PSNR、SSIM与SAM 文章目录 图像质量评价指标详解:PSNR、SSIM与SAM1. 峰值信噪比(PSNR)1.1 数学定义1.2 特点与局限性 2. 结构相似性指数(SSIM)2.1 数学定义2.2 特点与应用 3. 光谱角度映射器(SAM)3.1 数学定义3.2 特点与应用 4. Py…...
轻舟系列FPGA加速卡:大模型分布式训练中的高效协同者
在超大规模模型(如千亿级参数)的分布式训练中,计算、存储与通信的协同优化是突破性能瓶颈的关键。绿算技术公司的轻舟系列FPGA加速卡凭借其低延迟、高能效和可编程特性,能够成为分布式训练架构中的异构加速节点。其在训练集群中的…...
C++20 小语法
这个提案允许在static_assert和if constexpr中从整形转换为布尔类型。 以下表格就可以表示所有内容。 对于严格的C 编译器来说,以前在这种情境下int无法向下转换为bool,需要手动强制转换, C23 这一情况得到了改善。 对于严格的C编译器来说&a…...
LM393比较器的比较翻转电压不对
问个问题,用的LM393比较器,3.3V供电,比较器输出上拉到3.3V, V给的基准2.8V,V-电压从1V升到2.3V,比较器就输出0V了,按理论超过2.8V才翻转到0V的 根据问题描述和电路分析,比较器LM393…...
解决 shadui组件库Popover 点击后会消失
react用了shadui组件库 <Popover><PopoverTrigger><div className"text-operation-item" onClick{props.callback}><img src{props.imgSrc} width{20} height{20} /></div></PopoverTrigger><PopoverContent className"…...
国联股份卫多多与北京慧闻科技(集团)签署战略合作协议
4月27日,北京慧闻科技(集团)有限公司(以下简称“慧闻科技”)销售总监王兴卓一行到访国联股份卫多多,同卫多多/纸多多副总裁、产发部总经理段任飞,卫多多机器人产业链总经理桂林展开深入交流&…...
从数据到决策:如何使用Python进行自动驾驶数据分析
从数据到决策:如何使用Python进行自动驾驶数据分析 大家好,我是Echo_Wish,今天来和大家聊一聊在自动驾驶领域中,如何通过Python进行数据分析。随着自动驾驶技术的不断发展,数据分析在这一领域的作用越来越重要。从传感器数据的处理到模型的训练和优化,Python在自动驾驶数…...
IIS服务器提示ERR_HTTP2 PROTOCOL ERROR解决方案
今天我的淘宝店来了一个客户,说小程序苹果访问没问题,安卓系统访问出现ERR HTTP2 PROTOCAL ERROR的错误见下图 方法供大家闭坑步骤:通过注册表修改(高级) 打开注册表编辑器 (regedit) 导航到: HKEY_LOCAL_MACHINE\SYSTEM\Current…...
Django的异步任务队列管理_Celery
1 基本原理 Celery 是一个异步任务队列,能够将耗时操作(如发邮件、处理图片、网络爬虫等)从 Django 主线程中分离出来,由后台的 worker 处理,避免阻塞请求。Celery 作为独立运行的后台进程(Worker…...
Ubuntu18.04安装IntelliJ IDEA2025步骤
1.下载linux版本的idea 复制下面链接到虚拟机浏览器 下载 IntelliJ IDEA 下载好后,你的文件里会多一个文件,如下图 2.解压并运行Idea 2.1在/usr/local/路径下新建安装目录IDEA: 打开终端输入以下命令: sudo mkdir -p /usr/loc…...
LLM - Large Language Model
回顾2024:与LLM又相伴一年的经历与思考 - 知乎万字长文入门大语言模型(LLM) - 知乎“大模型本质就是两个文件!”特斯拉前AI总监爆火LLM科普,时长1小时,面向普通大众 - 知乎大模型本质及趋势剖析,…...
解决Ubuntu20.04重启出现显卡驱动异常的问题(操作记录)
一、问题情况 电脑异常断电,重启后,显示界面异常,显卡驱动已丢失。 二、操作流程 1、查看安装显卡驱动 ls /usr/src | grep nvidia 记住该驱动,我的是nvidia-550.127.05 2、安装dkms(如果没有) sudo …...
23.开关电源干扰控制的EMC改善措施
开关电源干扰控制的EMC改善措施 1. 开关电源的EMI干扰机理2. 钳位抑制EMI3. 阻容吸收抑制EMI4. 波形整形抑制EMI 1. 开关电源的EMI干扰机理 2. 钳位抑制EMI 只能抑制Ts阶段。 3. 阻容吸收抑制EMI 4. 波形整形抑制EMI...
新能源汽车声纹监测技术的发展趋势是什么?
新能源汽车声纹监测技术有以下发展趋势: 智能化与自动化程度不断提高 故障自动诊断与预警:未来声纹监测系统将能够更加准确地自动识别和分析新能源汽车各部件的声纹特征变化,不仅能检测出故障,还能对故障的发展趋势进行预测&…...
如何获取按关键字搜索京东商品详情(代码示例)
在电商领域,获取京东商品的详细信息对于市场分析、选品上架、库存管理和价格策略制定等方面至关重要。京东作为国内知名的电商平台,提供了丰富的商品资源。通过 Python 爬虫技术,我们可以高效地获取京东商品的详细信息,包括商品名…...
C#中构造器及属性的加载顺序
一.基本原则: 先加载静态构造函数和静态字段,后加载普通构造函数和普通字段;先加载基类再加载子类; 二.具体的加载顺序: 父类静态字段--->父类静态构造函数--->子类静态字段--->子类静态构造函数--->父类实例字段---> 父类实例构造函数--->子类实例字段-…...
vue项目中如何使用markdown编辑器
在开发中,编辑markdown和回显markdown都是常见的需求。在vue(2.x和3.x都可以)中可以使用mavon-editor这个第三方依赖包。示例很简单易懂。 安装 npm install mavon-editor 注册 在main.js中注册这个组件 import Vue from vue import mavonEditor from mavon-editor impor…...
Python爬虫(9)Python数据存储实战:基于pymysql的MySQL数据库操作详解
目录 一、背景与核心价值二、pymysql核心操作详解2.1 环境准备2.2 数据库连接与基础操作2.3 事务处理与错误回滚2.4 高级功能:批量插入与性能优化 三、pymysql进阶技巧3.1 连接池管理(推荐使用DBUtils)3.2 SQL注入防御3.3 与ORM框架对比 四、…...