7、Rust程序设计语言——常见集合

📅 2026/7/18 4:39:19
7、Rust程序设计语言——常见集合
目录1. Vector1.1 新建vector1.2 更新vector1.3 读取vector的元素1.4 遍历vector中的元素1.5 使用枚举储存多种类型1.6 丢弃vector时也会丢弃其所有元素2. String2.1 定义字符串2.2 新建字符串2.3 更新字符串2.3.1 使用push_str和push追加字符串2.3.2 使用运算符或format!宏拼接字符串2.4 索引字符串2.4.1 内部表现2.4.2 字节、标量值和字形簇2.5 字符串slice2.6 遍历字符串2.7 处理字符串的复杂性3. Hash Map3.1 新建一个HashMap3.2 访问HashMap中的值3.3 在HashMap中管理所有权3.4 更新HashMap3.4.1 覆盖一个值3.4.2 只在键上不存在时插入键值对3.4.3 根据旧值更新一个值3.5 哈希函数参考1. VectorVecT也称为vector允许你在单个数据结构中存放多个值并把这些值在内存中彼此相邻地排列起来。vector只能存储相同类型的值。1.1 新建vector要创建一个新的空vector可以调用Vec::new函数letv:Veci32Vec::new();这里必须添加类型注解否则Rust不知道我们打算存储什么类型的元素(这里初始化了一个不可变的vector变量之后也不能往里面增加值)。vector是使用泛型实现的当创建存放特定类型的vector时必须在尖括号中指定这个类型。泛型后面讲。更常见的情况是使用初始值创建vecTRust会自动推断你想要存储的值的类型。Rust还提供了vec!宏它会创建一个新的vector并把你提供的值放进去。letvvec![1,2,3];1.2 更新vectorletmutvVec::new();v.push(5);v.push(6);v.push(7);v.push(8);和上面不同这里初始化了一个可变的vector变量Rust根据放入的数据推断类型。1.3 读取vector的元素两种方式1. 索引 2. get方法。letvvec![1,2,3,4,5];letthird:i32v[2];println!(The third element is {third});letthird:Optioni32v.get(2);matchthird{Some(third)println!(The third element is {third}),Noneprintln!(There is no third elememnt.),}vector的索引是从0开始的使用和[]会得到索引位置处元素的引用。通过将索引作为参数传给get方法时会得到一个OptionT。Rust提供两种引用元素的方式是为了让你可以选择当尝试使用超出已有元素范围的索引值时程序如何表现。letvvec![1,2,3,4,5];letdoes_not_existv[100];letdoes_not_existv.get(100);第一种[]方法会让程序panic如果你希望程序在有人尝试访问vector末尾之外的元素时直接崩溃这种方式就很合适。去除[]方法运行letvvec![1,2,3,4,5];letdoes_not_existv.get(100);println!({:?},does_not_exist);当传给get方法的索引超出了vector的范围不会panic而是返回None。如果在正常情况下访问超出vector范围的元素偶尔是可能发生的就用这种方法。可以match处理Some(element)和None两种情况。之前将所有权和借用规则的时候提到过一个规则在同一作用域中不能同时拥有可变引用和不可变引用。letmutvvec![1,2,3,4,5];letfirstv[0];v.push(6);println!(The first element is {first});vector会把值彼此相邻地存放在内存中所以末尾追加一个新元素而当前存放位置又没有足够空间容纳所有元素程序就需要分配一块新内存并把旧元素复制到新空间中。在这种情况下原来指向某个元素的引用就会指向已释放的内存。借用规则正是为了防止程序陷入这种情况。1.4 遍历vector中的元素letvvec![100,32,57];foriinv{println!({i});}也可以遍历可变vector中每个元素的可变引用从而修改元素letmutvvec![100,32,57];foriinmutv{*i50;println!({i});}要修改可变引用所指向的值在使用运算符前必须使用解引用运算符*取到i指向的值。后面会将解引用运算符。由于借用检查器的规则不管是可变还是不可变地遍历vector都是安全的。for循环持有的那个对vector的引用会阻止对整个vector的同时修改。1.5 使用枚举储存多种类型vector只能存储相同类型的值。但是可以通过枚举来实现存储不同种类的元素因为枚举的各个变体都定义在同一个枚举类型之下。假如我们想要从电子表格的一行中读取值这一行中可能包含整数、浮点数、字符串。可以定义一个枚举让它的各个变体持有不同类型的值而所有的枚举变体都会被视为同一种类型也就是枚举类型本身。enumSpreadsheetCell{Int(i32),Float(f64),Text(String),}letrowvec![SpreadsheetCell::Int(3),SpreadsheetCell::Text(String::from(blue)),SpreadsheetCell::Float(10.12),];Rust必须在编译时知道vector中会有哪些类型这样它才能知道在堆上存储每个元素需要多少内存。还必须明确指出这个vector允许哪些类型。如果Rust允许vector存放任意类型那么在对vector元素执行操作时就有可能因为某一种或多种类型导致错误。如果在编写程序时你并不知道运行时究竟会有哪些类型需要存进vector那么这种枚举技巧就不适用了。需要使用trait对象后面讲。1.6 丢弃vector时也会丢弃其所有元素{letvvec![1,2,3,4];// 使用v}// - 在这里v离开作用域并被释放当vector丢弃时它包含的所有内容也都会被一并丢弃这意味着它持有的整数会被清理掉。使用借用检查器会确保对vector内容的任何引用都只会在vector本身有效时被使用。2. String2.1 定义字符串Rust的核心语言中只有一种字符串类型字符串slice str它通常以被借用的形式出现str。字符串slices是一些对储存在别处的UTF-8编码字符串数据的引用。由于字符串字面值被储存在程序的二进制输出中它们是字符串slices。字符串String类型由Rust标准库提供它是一种可增长、可变、可拥有、UTF-8编码的字符串类型。2.2 新建字符串String被实现为一个带有一些额外保证、限制和功能的字节vector封装。letmutsString::new();新建了一个叫s的字符串接着我们可以向其中加载数据。通常字符串会有初始数据我们希望一开始就有这个字符串。可以使用to_string方法它能用于任何实现了Display trait的类型比如字符串字面值。letdatainitial contents;letsdata.to_string();letsinitial contents.to_string();也可以使用String::from函数来从字符串字面值创建StringletsString::from(initial contents);字符串应用广泛这里有很多不同的用于字符串的通用API可供选择。如何选择是代码风格与可读性的问题。字符串是UTF-8编码的可以包含任何经过正确编码的数据lethelloString::from(السلام عليكم);lethelloString::from(Dobrý den);lethelloString::from(Hello);lethelloString::from(שלום);lethelloString::from(नमस्ते);lethelloString::from(こんにちは);lethelloString::from(안녕하세요);lethelloString::from(你好);lethelloString::from(Olá);lethelloString::from(Здравствуйте);lethelloString::from(Hola);2.3 更新字符串String的大小可以增加内容可以改变。可以使用或format!宏来拼接String值。2.3.1 使用push_str和push追加字符串可以通过push_str方法来附加字符串slice从而使String变长letmutsString::from(foo);s.push_str(bar);执行完这两行代码后s将会包含foobar。push_str方法采用字符串slice因为我们并不需要获取参数的所有权。letmuts1String::from(foo);lets2bar;s1.push_str(s2);println!(s2 is {s2});push方法被定义为获取一个单独的字符作为参数并附加到String中。letmutsString::from(lo);s.push(l);2.3.2 使用运算符或format!宏拼接字符串将两个已知的字符串合并在一起。一种方式是使用运算符lets1String::from(Hello, );lets2String::from(world!);lets3s1s2;// 注意s1被移动了不能继续使用运算符使用了add函数函数签名如下fnadd(self,s:str)-String{add的定义使用了泛型和关联类型上面函数签名正是替换为了String之后的具体方法。s2使用了意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数的s参数只能将str和String相加不能将两个String相加。但是s2的类型是String而不是add第二个参数指定的str。Rust会使用一种叫做deref强制转换的机制将s2转换成s2[…]也就是str。由于add不会获取s参数的所有权所以这个操作之后s2还是一个有效的String。add获取self的所有权因为self没有。let s3 s1 s2;会获取s1的所有权附加上从s2中拷贝的内容并返回结果的所有权。通过运算符级联多个字符串很笨重lets1String::from(tic);lets2String::from(tac);lets3String::from(toe);letss1-s2-s3;对于上面这个复杂的字符串链接可以使用format!宏lets1String::from(tic);lets2String::from(tac);lets3String::from(toe);letsformat!({s1}-{s2}-{s3});format!与println!的工作原理相同不过不同于将输出打印到屏幕上它返回一个带有结果内容的String。format!生成的代码使用引用因此不会获取任何函数的所有权。2.4 索引字符串在Rust中通过索引语法访问String的一部分会报错lets1String::from(hi);leths1[0];错误和提示说明了问题Rust的字符串不支持索引。2.4.1 内部表现String是一个Vecu8的封装。假如字符串如下lethelloString::from(Hola);在这里len的值为4存储Hola的vector长度为四个字节每个字母的UTF-8编码都占用一个字节。下面这个字符串中的首字母是西里尔字母的Ze而不是数字3。lethelloString::from(Здравствуйте);Rust中上面字符串的长度是24这是UTF-8编码上面字符串所需要的字节数因为这个字符串中每个Unicode标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的Unicode标量值。lethelloString::from(Здравствуйте);letanswerhello[0];answer不是第一个字符З。当使用utf-8编码时З的第一个字节是208(UTF-8编码对应的十进制原始字节值)第二个是151所以answer实际上应该是208不过208自身并不是一个有效的字母。返回208并没有任何实际意义。为了避免返回意外的值并造成不能立刻发现的bugRust不会编译这些代码。2.4.2 字节、标量值和字形簇从Rust的角度来说有三种方式可以查看字符串字节、标量值和字形簇(最接近人们眼中字母的概念)。比如用梵文书写的印度语单词“नमस्ते”最终储存在vector中的u8值如下[224,164,168,224,164,174,224,164,184,224,165,141,224,164,164,224,165,135]这里有18个字节也就是计算机最终会储存的数据。如果从unicode标量值的角度来理解它们也就像Rust的char类型(char对应Unicode标量值占4个字节)这些字节看起来就像这样[न,म,स,्,त,े]这里有六个char不过第四个和第六个都不是字母它们是发音符号本身并没有意义。如果以字形簇的角度理解就会得到人们所说的构成这个单词的四个字母[न,म,स्,ते]Rust不允许使用索引获取String字符的另一个原因是索引操作必须满足 O(1) 常数时间而 UTF-8 变长编码决定了定位第 N 个字符必须从头遍历到目标位置才能确定边界无法常数跳转因此 Rust 在语法层面直接禁用了索引。2.5 字符串slice索引字符串通常是一个坏点子因为字符串索引应该返回的类型是不明确的字节值、字符、字形簇或者字符串slice。为了明确索引并表明你需要一个字符串slice相比于使用[]和单个值的索引Rust强制使用[]和一个range来创建含特定字节的字符串slicelethelloString::from(Здравствуйте);letanswerhello[0..4];println!({answer});这里s是str包含字符串的头四个字节。而这些字母都是两个字节长的s是Зд。如果尝试用类似hello[0…1]的方式对字符的部分字节进行sliceRust会在运行时panic因此使用range来创建字符串slice时要格外小心。2.6 遍历字符串操作字符串的每一部分的最好的方法是明确表示需要字符还是字节。对于单独的Unicode标量值使用chars方法。forcinЗд.chars(){println!({c});}bytes方法返回每一个原始字节forbinЗд.bytes(){println!({b});}chars和bytes方法都只拿到了self的不可变借用因此无法在循环中修改原字符串。有效的Unicode标量值可能会由不止一个字节组成。从字符串中获取如同天城文这样的字形簇是复杂的标准库没有提供这个功能。crats.io上有提供这样功能的crate。2.7 处理字符串的复杂性Rust选择把正确处理String数据作为所有Rust程序的默认行为这意味着程序员必须在一开始就更多地思考如何处理UTF-8数据。这种权衡比其他编程语言更直接暴露了字符串的复杂性但它能避免你在开发周期的后期再去处理那些涉及非ASCII字符的错误。标准库围绕String和str构建了很多功能来帮助我们正确处理这些复杂场景。3. Hash MapHashMapK, V类型存储了一个键类型K对应一个值类型V的映射。它通过一个哈希函数来实现映射决定如何将键和值放入内存中。HashMap可以用于需要任何类型作为键来寻找数据的情况而不是像vector那样通过索引。3.1 新建一个HashMap可以使用new创建一个空的HashMap并使用insert增加元素。假设有两个队伍需要记录分数分别是蓝队和黄队蓝队有10分黄队有50分usestd::collections::HashMap;letmutscoresHashMap::new();scores.insert(String::from(Blue),10);scores.insert(String::from(Yellow),50);HashMap没有被prelude自动引用需要使用use创建快捷路径。HashMap的数据储存在堆上并且所有的键必须是相同类型所有的值也必须是相同类型。3.2 访问HashMap中的值通过get方法并提供对应的键来从HashMap中获取值usestd::collections::HashMap;letmutscoresHashMap::new();scores.insert(String::from(Blue),10);scores.insert(String::from(Yellow),50);letteam_nameString::from(Blue);letscorescores.get(team_name).copied().unwrap_or(0);println!({score});for(key,value)inscores{println!({key}:\t{value});}遍历HashMap会以任意顺序打印出每一个键值对。3.3 在HashMap中管理所有权对于实现了Copy trait的类型其值可以拷贝进HashMap。对于像String这样拥有所有权的值其值将被移动而HashMap会成为这些值的拥有者。usestd::collections::HashMap;letfield_nameString::from(Favorite color);letfield_valueString::from(Blue);letmutmapHashMap::new();map.insert(field_name,field_value);// 这里field_name和field_value不再有效// 尝试使用它们看看会出现什么编译错误println!({field_name}: {field_value});当insert调用将field_name和field_value移动到HashMap中将不能使用这两个绑定。如果我们把对值的引用插入HashMap这些值本身并不会被移动进HashMap。引用所指向的值必须至少在HashMap有效的那段时间里一直有效。后面生命周期会详细讨论这个问题。3.4 更新HashMap尽管键值对的数量是可以增长的每个唯一的键只能同时关联一个值。当我们想要改变HashMap中的数据时必须决定如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值并只在键没有对应值时增加新值。或者可以结合新旧两值。3.4.1 覆盖一个值如果我们插入了一个键值对接着用相同的键插入一个不同的值与这个键相关联的旧值将被替换。usestd::collections::HashMap;letmutscoresHashMap::new();scores.insert(String::from(Blue),10);scores.insert(String::from(Blue),25);println!({scores:?});3.4.2 只在键上不存在时插入键值对我们经常会检查某个特定的键是否已经在哈希map中有对应的值然后执行如下操作如果这个键已经存在就让原来的值保持不变如果这个键不存在就插入它和它对应的值。HashMap为这种场景提供了一个特殊的API叫作entry它接受你想检查的键作为参数。entry方法的返回值是一个名为Entry的枚举它表示一个可能存在、也可能不存在的值。usestd::collections::HashMap;letmutscoresHashMap::new();scores.insert(String::from(Blue),10);scores.entry(String::from(Yellow)).or_insert(50);scores.entry(String::from(Blue)).or_insert(50);println!({scores:?});Entry上的or_insert方法被定义为如果对应Entry的键已经存在就返回该值的可变引用如果不存在就把参数作为这个键的新值插入并返回这个新值的可变引用。3.4.3 根据旧值更新一个值更常见的场景是找到一个键对应的值并根据旧的值更新它。比如记录单词出现的次数。usestd::collections::HashMap;lettexthello world wonderful world;letmutmapHashMap::new();forwordintext.split_whitespace(){letcountmap.entry(word).or_insert(0);*count1;}println!({map:?});split_whitespace方法返回一个由空格分隔text值子slice的迭代器。or_insert方法返回这个键的值的一个可变引用并储存在count变量中为了赋值使用星号*解引用count。这个可变引用在for循环的结尾离开作用域这样所有这些改变都是安全的并符合借用规则。3.5 哈希函数HashMap默认使用一种叫做SipHash的哈希函数它可以提供对涉及哈希表的拒绝服务攻击的抵抗能力。不过这不是目前可用的最快哈希算法但为了更好的安全性而接受一些性能下降是值得的权衡。可以指定不同的hasher来切换到其他函数。hasher是一种实现了BuildHasher trait的类型。参考1、常见集合