rust提供了多种数据结构来帮助存储数据,他们都是可变长度的集合
Vector
vector 允许我们以可变长度存储一系列相同的值,类似于可变长度的数组,它在创建时需要指定存储的数据的类型,或者指定初始值使得编译器能够分析出其中存储的类型
//新建一个空的
let v: Vec<i32> = Vec::new();
//设置初始值
let v = vec![1, 2, 3];
你可以将 vector 申明为可变的,这样就可以使用push方法可以向其中新增元素
let mut v = Vec::new();
v.push(5);
以下是一些其他api的使用方式
vec.is_empty() //判空,返回bool值
[1].repeat(5) //[1,1,1,1,1],一个1重复5次的vector
v.len() //长度
//追加一组数据
let mut vec5 = [1].repeat(2);
vec.append(&mut vec5);
//追加一组数据
vec.extend([1, 2, 3].iter().copied());
//删除
vec.remove(vec.len() - 1);
//排序
vec.sort();
//寻找是否存在某个数据,返回下标bool
let ss = vec.contains(&1);
再获取一个 vector 中的元素时,可以使用下标访问它,但是对于 String 等非标量的数据结构,直接访问它会发生所有权转移的问题
//这是可以的
let v = vec![1, 2, 3];
let third = v[2];
//这是报错的
let v = vec![String::from("a"), String::from("a"), String::from("a")];
let third = v[2];
为了解决这个问题,我们通过会引用 vector 中的元素在,引用 vector 的元素时,可以选择使用 &引用或者 get 的方式来引用它,区别是 &引用 是返回 vector中某个元素的引用,如果这个元素不存在,会发生越界的错误;而 get 函数返回一个 Option 指针 ,如果你访问的元素不存在不会产生问题:
let v = vec![1, 2, 3];
//引用
let third: &i32 = &v[2];
//get函数
match v.get(2) {
//这里的third是一个指针
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
但是如果我们获取了 vector 中一个数的引用,再去改变这个 vector 可能会引发问题,因为改变 vector 可能会引发内存的重新发配,而引用的地址的信息可能因此发生改变,从而产生异常。你可以通过之前处理String 相同的克隆一份数据的方式来获取数据:
//这样是报错的
let v = vec![1, 2, 3];
let third: &i32 = &v[2];
v.push(2);
//clone一份数据
let mut v = vec!["a".to_string(), "a".to_string(), "a".to_string()];
let mut first = v[0].clone();
v.push(String::from("aaa"));
使用for循环可以遍历 vector 中的元素,因为是先引用再遍历,每一个 i 的返回值都是引用,如果数组可变,也可以返回可变的引用,通过 *
解引用的方式可以直接改变每一个引用指向的内容的数值,这个在后续会提到:
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;
}
String
在rust中有两种字符串:
-
String就像是Vec一样是动态分配在堆heap上的字符串类型,它指向一串UTF8编码的字节序。使用一些 String 自带的api可以创建一个String。
-
&str 是一个指向分配在某处的 String 的一个固定容量的视图,或者是一个使用 “ ” 定义的字符串字面量,不同的是前者指向String的内容,在堆上,后者指向静态内存,因为是引用,所以他没有字符串的所有权。
-
使用 to_string() 可以将str转换为 String
-
使用 [ a…b ] 可以将 String 转化成切片 , 同时在 rust 中 &String 可以直接转为 &str 传入各个函数 api 中
//创建空字符串
let mut s = String::new();
//新建一个字符串
let s = String::from("initial contents");
//创建一个字符串字面量 &str
let data = "initial contents";
//转换成String
let s = data.to_string();
//转化为 &str
let str = &s[0..2];
//可以直接转化
let str = &s;
let e_0 = String::from("hello world");
let e = e_0.as_str();
String 的大小可以增加,其内容也可以改变,可以通过 push_str
方法来字符串常量,从而使 String
变长,这个方法也可以使用 + 符号来实现,但是 + 符号的第二个参数需要是一个 &str
let mut s = String::from("foo");
s.push_str("bar");
//上下的作用相同
let s1 = String::from("foo");
let s2 = String::from("bar");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
使用 format! 可以快速拼接多个字符串
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
在rust中不能随意获取一个字符串中的一个字符串,因为 rust 使用 unicode 编码,所以有些语言一个字母不一定存一个字节,有时候是两个:
let hello = String::from("Здравствуйте");
hello.len(); // 24
//因为第一个字母占两个字节,所以这类代码是不可以通过的
let answer = &hello[0];
//这样的代码是可以通过的,因为刚好截取了正确的字母
let answer = &hello[0..2];
//这样的代码是不可以通过的,因为刚好截取一个字母的一半
let answer = &hello[0..1];
对于字符串可以使用不同的方式进行遍历,可以按照一个一个 unicode 字节或者一个一个字符进行遍历
for c in "नमस्ते".chars() {
println!("{}", c);
}
//按照字符遍历न , म ....
for b in "नमस्ते".bytes() {
println!("{}", b);
}
//按字节遍历 224 ,164.....
HashMap
HashMap<K, V>
类型储存了一个键类型 K
对应一个值类型 V
的映射,其中的每个元素都按照 k,v 对的形式存储,根据唯一的 k可以找对应的 v ,可以使用 new
创建一个空的 HashMap
,并使用 insert
增加元素,在第一次新增元素之后,编译器就会将 HashMap 的类型固定,不能再插入其他类型的键值对
use std::collections::HashMap;
let mut a = HashMap::new();
a.insert(String::from("1"), 10);
另一个构建哈希 map 的方法是在一个元组的 vector 上使用迭代器(iterator)和 collect
方法,其中每个元组包含一个键值对,这将在后续介绍。
let t = vec![String::from("a"),String::from("b")];
let i = vec![10,20];
//必须有HashMap<_,_> ,collect可以返回多种数据结构
let hash : HashMap<_,_> = t.iter().zip(i.iter()).collect();
如果 insert 一个已经存在的 key 那么会直接更新它,所以可以使用 entry ( ) 和 or_insert( ) 函数进行检查 , 这样的组合只有再某个key值不存在的时候才会插入,存在则不会改变值,同时它可以返回指定 key 的 val ,它是一个引用,如果想要改变它的值需要解引用
let val = a.entry(String::from("1")).or_insert(50);
*val += 1;
可以通过 get
方法并提供对应的键来从哈希 map 中获取值,返回 Option<V>
let t = String::from("1");
let ta = a.get(&t);
可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对
for (key, value) in &a {
println!("{}: {}", key, value);
}
泛型
我们可以使用泛型为像函数签名或结构体这样的项创建定义,可以把它理解成是一个占位符,直到我们需要具体定义使用某个结构时,将具体的数据类型填充进去,所有使用泛型占位的地方都将成为该类型。使用泛型可以编写不重复的代码,而 Rust 将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。
如以下的例子,T是那个泛型即占位符,传入一个 i32 的数组之后,T就改写为了 i32
//泛型定义
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
let number_list = vec![34, 50, 25, 100, 65];
//传入一个数字
let result = largest(&number_list);
但是如果将数组变成字符串,则会产生报错,因为不能适用于 T
的所有可能的类型。因为在函数体需要比较 T
类型的值,不过它只能用于我们知道如何排序的类型。
在一个定义中,可以包含多个泛型,仅需要一一对应即可,在结构体、函数、枚举中都可以使用泛型
//单个泛型
struct Point<T> {
x: T,
y: T,
}
//多个泛型
struct Point<T, U> {
x: T,
y: U,
}
//枚举泛型
enum Result<T, E> {
Ok(T),
Err(E),
}