之前用到的 String 类型,并没有那么简单,接下来会聊一下关于 String 更多的一些应用。在说 String 前,需要先学习一下 Vec 这种数据类型,类似于一个动态的数组。然后是 HashMap,一个键对值的数据类型,与其他编程语言中的字典很类似。

Vec

Vec<T> 和数组一样,用于存储一系列相同类型的值。但是 Vec 可以动态地插入,删除。首先,是创建一个 Vec,可以使用 Vec::new(),或者使用宏 vec!。要注意的是,只有使用 mut,才能使 Vec 可变,也就是可以插入和删除值。

fn main() {
    // 在定义时就标明数据类型为 i32 的 Vec
    let v1: Vec<i32> =  Vec::new();

    // 在定义时不标明类型,而在首次插入值时,由Rust自动推断
    let mut v2 = Vec::new();

    // 这里插入了一个 i32 的值,所以Rust推断 v2 为存放 i32 的 Vec
    v2.push(2);

    let v3 = vec![1,2,3,4,5];

    let v4 = vec!["hello", "rust"];

    let mut v5: Vec<String> = vec!["hello".to_string(), "rust".to_string()];
}

向 Vec 有两个操作函数,一个是 push,往里插入值,一个是 pop,往外弹出值,Pop返回的是最后插入的值。看下面的代码

fn main() {
    let mut v1 = Vec::new();
    v1.push(2);
    v1.push(3);
    v1.push(4);
    let x = v1.pop();

    println!("{:?}", x);   // Some(4);
}

为什么是 Some(4) 而不是 4呢,因为 pop 返回的是 Option<T> 类型的。我们也可以像访问数组一样,使用 [索引] 的形式来访问 Vec 中的值

fn main() {
    let mut v1 = Vec::new();
    v1.push(2);
    v1.push(3);
    v1.push(4);
    let x = v1[1];

    println!("{:?}", x);   // 3

    v1[0] = 100;
    println!("{:?}", v1);  // [100, 3, 4]
}

可以看出,使用索引的形式访问 Vec,返回的就是里面的值,而不是 Option<T> 类型。但是,使用索引访问时,如果索引越界,将导致 panic。如果要更新直接 Vec 中的值,也可以直接使用索引的形式。

接下来就是遍历,看下面的代码

fn main() {
    let mut v1 = Vec::new();
    v1.push(2);
    v1.push(3);
    v1.push(4);
    
    for x in v1.iter() {
        println!("{}", x);
    }
}

遍历我们使用 iter() 这个函数,代码中的 x 是对 v1中每一个值的引用,&i32 类型。以上就是 Vec 的常用内容,更详细的方法直接看官方文档

String

对于 String,在之前我们已经使用过

fn main() {
    // 下面两种形式创建一个 String
    let mut s1 = String::from("hello");
    let mut s2 = "hello".to_string();

    // push_str 可以拼接一个字符串
    s1.push_str(" Rust");
    println!("s1: {}", s1);

    // push 可以拼接一个字符
    s2.push(' ');
    s2.push('A');

    println!("s2: {}", s2);
}

我们也可以使用下面的方式来拼接字符串

fn main() {
    let s1 = String::from("Hello ");
    let s2 = s1 + "Rust";

    println!("s2: {}", s2);
    // println!("s1: {}", s1); // 这里不能再使用 s1,因为 s1 的所有权已经移动
}

String 类型可以使用 + 操作,注意第一个参数,一定是一个 String,而后面的参数,都是 &str,感觉内部对于 + 的实现,就是调用了,push_str。除此之外,还可以使用一个宏来拼接字符串

fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from("Rust");
    let s3 = format!("{} {}", s1, s2);
    println!("s1: {}", s1);
    println!("s2: {}", s2);
    println!("s3: {}", s3);
}

format! 这个宏并没有获取 s1,s2 的所有权。接下来我们看一个和其他编程语言中的字符串不一样的地方。

fn main() {
    let s1 = String::from("Hello");
    println!("{}", s1.len());  // 5

    let s2 = String::from("你好");
    println!("{}", s2.len());  // 6
}

上面的代码中,s2 的长度是 6,而不是2,这是为什么呢。这里因为,Rust 中的 String 是 UTF-8 编码的,它可以包含任何可以正确编码的数据,例如下面的代码

let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");

第一段代码中,“Hello” 是五个字母,每一个字母的 UTF-8 编码都占用一个字节,所以s1的长度是 5,对于 Rust 来说,len() 函数取的不是字符的长度,而是字节的长度,这一点与其他编程语言不太一样。而 “你好”,显然,每一个字被编码为三个 UTF-8 字节,所以长度为 6。

对于 Rust 来说,关于字符串,有三个概念,字节标量值字形簇。例如 “नमस्ते”,从节字的角度来说,它是 [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]。而从标量值(char)的角度来说,它是 [‘न’, ‘म’, ‘स’, ‘्’, ‘त’, ‘े’]。最后,如果从字形簇的角度来说,它就是原文中的样子 [“न”, “म”, “स्”, “ते”]。字形簇是最接近我们常用的字母的概念。

字符串是不允许使用 [索引] 的形式访问单个字符的,也是因为 String 存储的是 UTF-8 编码的数据这种特性,因为如果允许使用索引,可能导致访问到字符一个字符的中间,就造成了无效索引。

字符串的遍历,可以使用 chars() 这个函数,它会返回这个字符串的 Unicode 标题值。

fn main() {
    let s1 = String::from("नमस्ते");
    for x in s1.chars(){
        println!("x: {}", x);
    }
}

如果要访问每一个 UTF-8 值,也可以使用 bytes() 方法

fn main() {
    let s1 = String::from("नमस्ते");
    for x in s1.bytes(){
        println!("x: {}", x);
    }
}

HashMap

HashMap 是一个键对值的存储结构,键保持唯一,对,它就像其他编程语言中的字典。

use std::collections::HashMap;

fn main() {
    // 创建一个HashMap
    let mut map: HashMap<i32, i32> = HashMap::new();

    // 向HashMap中插入值
    map.insert(0, 0);
    map.insert(1, 1);
    println!("{:?}", map);  // {0: 0, 1: 1}

    // 如果插入重复Key的值,原值将被覆盖 (更新)
    map.insert(0, 10);
    println!("{:?}", map);  // {0: 10, 1: 1}

    // 判断是否包指定的 Key,这里传的是引用
    if map.contains_key(&0) {
        println!("Has Key 0");
    } else {
        println!("No key 0");
    }

    // 移除一指定 key 的键和值
    map.remove(&0);
    println!("{:?}", map);  // {1: 1}

    // 如果 map 中不存在指定的 key,则插入,否则不插入
    map.entry(0).or_insert(100);
    println!("{:?}", map);  // {0: 100, 1: 1}

    // 从 map 中获取一个值,注意返回的类型是 Option<T> 类型
    let first = map.get(&0);
    println!("{:?}", first);    // Some(100);
}