rust学习笔记之基础:模式匹配、常见集合、路径IO

模式和匹配

模式是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。结合使用模式和 match 表达式以及其他结构可以提供更多对程序控制流的支配权。模式由如下一些内容组合而成:

  • 字面量
  • 解构的数组、枚举、结构体或者元组
  • 变量
  • 通配符
  • 占位符

所有可能会用到模式的位置

match 分支

一个模式常用的位置是 match 表达式的分支。在形式上 match 表达式由 match 关键字、用于匹配的值和一个或多个分支构成,这些分支包含一个模式和在值匹配分支的模式时运行的表达式。

match 表达式必须是穷尽的,意为 match 表达式所有可能的值都必须被考虑到。一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式:比如,一个匹配任何值的名称永远也不会失败,因此可以覆盖所有匹配剩下的情况。

有一个特定的模式 _ 可以匹配所有情况,不过它从不绑定任何变量。

if let 条件表达式

if let 表达式主要用于编写等同于只关心一个情况的 match 语句简写。if let 可以对应一个可选的带有代码的 else 在 if let 中的模式不匹配时运行。

可以组合并匹配 if let、else if 和 else if let 表达式。这相比 match 表达式一次只能将一个值与模式比较提供了更多灵活性;一系列 if let、else if、else if let 分支并不要求其条件相互关联。

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {}, as the background", color);
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

注意 if let 也可以像 match 分支那样引入覆盖变量:if let Ok(age) = age 引入了一个新的覆盖变量 age,它包含 Ok 成员中的值。

if let 表达式的缺点在于其穷尽性没有为编译器所检查,而 match 表达式则检查了。

while let 条件循环

一个与 if let 结构类似的是 while let 条件循环,它允许只要模式匹配就一直进行 while 循环。

fn main() {
    let mut stack = Vec::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
}

pop 方法取出 vector 的最后一个元素并返回 Some(value)。如果 vector 是空的,它返回 None。while 循环只要 pop 返回 Some 就会一直运行其块中的代码。一旦其返回 None,while 循环停止。我们可以使用 while let 来弹出栈中的每一个元素。

for 循环

for 循环是 Rust 中最常见的循环结构。在 for 循环中,模式是 for 关键字直接跟随的值,正如 for x in y 中的 x。

fn main() {
    let v = vec!['a', 'b', 'c'];
    for (index, value) in v.iter().enumerate() {
        println!("{} is at index {}", value, index);
    }
}

这里使用 enumerate 方法适配一个迭代器来产生一个值和其在迭代器中的索引,他们位于一个元组中。第一个 enumerate 调用会产生元组 (0, 'a')。当这个值匹配模式 (index, value),index 将会是 0 而 value 将会是 'a',并打印出第一行输出。

let 语句
let x = 5;
let (x, y, z) = (1, 2, 3);

let x = 5; 这样的语句中变量名位于 PATTERN 位置,变量名不过是形式特别朴素的模式。x 是一个表示“将匹配到的值绑定到变量 x” 的模式。同时因为名称 x 是整个模式,这个模式实际上等于 “将任何值绑定到变量 x,不管值是什么”。

let (x, y, z) = (1, 2, 3);,元组模式看作是将三个独立的变量模式结合在一起。如果模式中元素的数量不匹配元组中元素的数量,则整个类型不匹配,并会得到一个编译时错误。如果希望忽略元组中一个或多个值,也可以使用 _..

函数参数

函数参数也可以是模式。类似于之前对 let 所做的,可以在函数参数中匹配元组。

fn foo(x: i32) {
    // 代码
}

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

Refutability 可反驳性 : 模式是否会匹配失效

模式有两种形式:refutable(可反驳的)和 irrefutable(不可反驳的)。能匹配任何传递的可能值的模式被称为是不可反驳的(irrefutable)。一个例子就是 let x = 5; 语句中的 x,因为 x 可以匹配任何值所以不可能会失败。对某些可能的值进行匹配会失败的模式被称为是可反驳的(refutable)。一个这样的例子便是 if let Some(x) = a_value 表达式中的 Some(x);如果变量 a_value 中的值是 None 而不是 Some,那么 Some(x) 模式不能匹配。

函数参数、 let 语句和 for 循环只能接受不可反驳的模式,因为通过不匹配的值程序无法进行有意义的工作。if let 和 while let 表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败:条件表达式的功能就是根据成功或失败执行不同的操作。

通常我们无需担心可反驳和不可反驳模式的区别,不过确实需要熟悉可反驳性的概念,这样当在错误信息中看到时就知道如何应对。遇到这些情况,根据代码行为的意图,需要修改模式或者使用模式的结构。尝试在 Rust 要求不可反驳模式的地方使用可反驳模式或者相反情况将不能通过编译:

let Some(x) = some_option_value; // 不可反驳模式的地方使用可反驳模式将不能通过编译

if let x = 5 { // 可反驳模式的地方使用不可反驳模式得到一个警告
    println!("{}", x);
};

基于此,match 匹配分支必须使用可反驳模式,除了最后一个分支需要使用能匹配任何剩余值的不可反驳模式。Rust 允许我们在只有一个匹配分支的 match 中使用不可反驳模式,不过这么做不是特别有用,并可以被更简单的 if let 语句替代。

模式语法

匹配字面量
fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}
匹配命名变量

命名变量是匹配任何值的不可反驳模式。然而当其用于 match 表达式时情况会有些复杂。因为 match 会开始一个新作用域,match 表达式中作为模式的一部分声明的变量会覆盖 match 结构之外的同名变量,与所有变量一样。

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {:?}", y),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {:?}", x, y);
}

第二个匹配分支中的模式引入了一个新变量 y,它会匹配任何 Some 中的值。因为我们在 match 表达式的新作用域中,这是一个新变量,而不是开头声明为值 10 的那个 y。这个新的 y 绑定会匹配任何 Some 中的值,在这里是 x 中的值。如果 x 的值是 None ,头两个分支的模式不会匹配,所以会匹配下划线。这个分支的模式中没有引入变量 x,所以此时表达式中的 x 会是外部没有被覆盖的 x。一旦 match 表达式执行完毕,其作用域也就结束了,同理内部 y 的作用域也结束了。

输出

Matched, y = 5
at the end: x = Some(5), y = 10
多个模式

在 match 表达式中,可以使用 | 语法匹配多个模式,它代表或。

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}
通过 ..= 匹配值的范围

..= 语法允许你匹配一个闭区间范围内的值。范围只允许用于数字或 char 值,因为编译器会在编译时检查范围不为空。char 和 数字值是 Rust 仅有的可以判断范围是否为空的类型。

fn main() {
    let x = 5;
    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }


    let x = 'c';
    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}
解构并分解值

解构结构体

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };
    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);

    match p {
        Point { x, y: 0 } => println!("On the x axis at {}", x),
        Point { x: 0, y } => println!("On the y axis at {}", y),
        Point { x, y } => println!("On neither axis: ({}, {})", x, y),
    }
}

解构枚举:

  • 解构枚举的模式需要对应枚举所定义的储存数据的方式
  • 对于像 Message::Quit 这样没有任何数据的枚举成员,不能进一步解构其值。只能匹配其字面量 Message::Quit,因此模式中没有任何变量。
  • 对于像 Message::Move 这样的类结构体枚举成员,可以采用类似于匹配结构体的模式。
  • 对于像 Message::Write 这样的包含一个元素,以及像 Message::ChangeColor 这样包含三个元素的类元组枚举成员,其模式则类似于用于解构元组的模式。模式中变量的数量必须与成员中元素的数量一致。
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        },
        Message::Move { x, y } => {
            println!("Move in the x direction {} and in the y direction {}", x, y);
        },
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => {
            println!("Change the color to red {}, green {}, and blue {}", r, g, b);
        }
    }
}

解构嵌套的结构体和枚举

enum Color {
   Rgb(i32, i32, i32),
   Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change the color to red {}, green {}, and blue {}", r, g, b);
        },
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change the color to hue {}, saturation {}, and value {}", h, s, v);
        },
        _ => ()
    }
}

解构结构体和元组

let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
忽略模式中的值

有时忽略模式中的一些值是有用的,比如 match 中最后捕获全部情况的分支实际上没有做任何事,但是它确实对所有剩余情况负责。有一些简单的方法可以忽略模式中全部或部分值:使用 _ 模式,在另一个模式中使用 _ 模式,使用一个以下划线开始的名称,或者使用 .. 忽略所剩部分的值。

只使用下划线 _ 本身,并不会绑定值。

// 使用 _ 忽略整个值。大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。
// 在一些情况下忽略函数参数会变得特别有用,比如实现 trait 时,当你需要特定类型签名但是函数实现并不需要某个参数时。
fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
}

// 使用嵌套的 _ 忽略部分值
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
    (Some(_), Some(_)) => {
        println!("Can't overwrite an existing customized value");
    }
    _ => {
        setting_value = new_setting_value;
    }
}
let numbers = (2, 4, 8, 16, 32);
match numbers {
    (first, _, third, _, fifth) => {
        println!("Some numbers: {}, {}, {}", first, third, fifth)
    },
}

// 通过在名字前以一个下划线开头来忽略未使用的变量
let _x = 5;
let y = 10;
let s = Some(String::from("Hello!"));
if let Some(_s) = &s {
    println!("{}",s);
}

// 用 .. 忽略剩余值
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
    Point { x, .. } => println!("x is {}", x),
}
let (first, .., last)=(2, 4, 8, 16, 32);
匹配守卫提供的额外条件

匹配守卫是一个指定于 match 分支模式之后的额外 if 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x < 5 => println!("less than five: {}", x),
        Some(x) => println!("{}", x),
        None => (),
    }
}

使用匹配守卫来解决模式中变量覆盖的问题

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {}", n),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {}", x, y);
}

在匹配守卫中使用 或 运算符 | 来指定多个模式,同时匹配守卫的条件会作用于所有的模式。

let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}
@ 绑定

at 运算符 @ 允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };
    match msg {
        Message::Hello { id: id_variable @ 3..=7 } => {
            println!("Found an id in range: {}", id_variable)
        },
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        },
        Message::Hello { id } => {
            println!("Found some other id: {}", id)
        },
    }
}

常见集合

Rust 标准库中包含一系列被称为集合(collections)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。

vector

vector 允许我们在一个单独的数据结构中储存多个值,所有值在内存中彼此相邻排列。vector 只能储存相同类型的值。vector 是用泛型实现的。vector 是大小可变的数组。和 slice(切片)类似,它们的大小在编译时是未知的,但它们可以随时扩大或缩小。一个 vector 使用 3 个词来表示:一个指向数据的指针、vector 的长度、还有它的容量。此容量指明了要为这个 vector 保留多少内存。vector 的长度只要小于该容量,就可以随意增长;当需要超过这个阈值时,会给 vector 重新分配一段更大的容量。

let v1: Vec<i32> = Vec::new(); // 创建一个新的空 vector
let v2 = vec![1, 2, 3]; // 使用 vec! 宏和初始值来创建一个 vector
let collected_iterator: Vec<i32> = (0..10).collect(); // 迭代器可以被收集到 vector 中
println!("v2[0] is {}", v2[0]); // 使用索引读取元素
match v2.get(0) { // 使用 get 方法以索引作为参数来返回一个 Option<&T>,接收到不存在的索引时返回 None。
  Some(value) => println!("value is {}", value),
  None => println!("no element"),
}

let mut v3 = Vec::new();
v3.push(5);  // 末尾追加元素‌
v3.insert(0, 8); // 在索引 0 处插入元素‌
println!("first element: {}", v3[0]); // 使用索引读取元素
v3.sort(); // 升序排序‌
println!("Vector: {:?}", v3);
println!("binary_search 8: {:?}", v3.binary_search(&8)); // 二分查找(需先排序)‌,返回Result类型
println!("remove : {}", v3.remove(1)); // 删除索引 1 的元素,返回元素值
println!("contains 8: {}", v3.contains(&8)); // 检查是否包含元素
println!("size: {}", v3.len()); // 获取当前元素数量‌
println!("capacity: {}", v3.capacity()); // 获取分配的内存容量‌
println!("Pop last element: {:?}", v3.pop()); // 移除末尾元素‌,返回Opton类型
println!("is_empty: {}", v3.is_empty()); // 检查是否为空

let mut vec = vec![1, 2, 3]; // 调整向量的长度为参数1,当需要扩大时,用参数2填充新增元素;当需要缩小时,直接截断多余元素
vec.resize(5, 0);    // [1, 2, 3, 0, 0]
vec.resize(2, 0);    // [1, 2]

注意 v1 的定义,我们增加了一个类型标注。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。放入 v3 的所有值都是 i32 类型的,而且 Rust 也根据数据做出如此判断,所以不需要 Vec 标注。

一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则确保 vector 内容的这个引用和任何其他引用保持有效。当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,这是行不通的:

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);

为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。

遍历 vector 中的元素
let v = vec![1, 2, 3];
for num in v { // 所有权转移,值遍历
    println!("{}", num);
}
// 此处不能再使用 `v`,因为所有权已转移给 `for` 循环

let v = vec![100, 32, 57];
for i in &v { //  不可变引用遍历
    println!("{}", i);
}
// 此处仍可使用 `v`

let mut v = vec![100, 32, 57];
for i in &mut v { // 可变引用遍历
    *i += 50; // 可修改元素
}

let mut xs = vec![1i32, 2, 3];
for x in xs.iter() {// 不可变引用迭代
    println!("> {}", x);
}
for (i, x) in xs.iter().enumerate() {// 不可变引用迭代,记录索引
    println!("In position {} we have value {}", i, x);
}
for x in xs.iter_mut() {// 可变迭代,其中每个值都能被修改
    *x *= 3;
}
println!("Updated vector: {:?}", xs);

类似于任何其他的 struct,vector 在其离开作用域时会被释放,丢弃 vector 时也会丢弃其所有元素。

使用枚举来储存多种类型
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 的核心语言中只有一种字符串类型:str。字符串 slice 通常以被借用的形式出现:&str。String 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。

在 Rust 中,String 类型实现了多个关键 trait 以支持字符串操作、类型转换和所有权管理。

  • AsRef<str>‌: 允许将 String 无损转换为 &str 切片,提供零成本引用转换,避免数据拷贝。
  • Deref<Target=str>‌: 通过自动解引用机制,实现方法继承,使 String 可直接调用 str 的方法。
  • Clone‌:创建 String 的完整独立副本(深拷贝)。复制堆上的字符串数据,满足所有权规则,避免悬垂指针。
  • PartialEq 和 Eq‌:支持字符串内容相等性比较。
  • From 和 Into‌:提供类型间安全转换
let mut s = String::new(); // 新建一个空的 String
let s = "initial contents".to_string(); // 使用 to_string 方法从字符串字面量创建 String
let s = String::from("initial contents"); // 使用 String::from 函数从字符串字面量创建 String

// 字符串是 UTF-8 编码的,所以可以包含任何正确编码的数据
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("你好");
let hello = String::from("Hola");

let s2:String="hello".into(); // 通过类型转换创建String,需指明类型
println!("s2 len: {}",s2.len()); // UTF-8字节长度‌,5
println!("s2 is_empty: {}",s2.is_empty()); // s2 is_empty: false
let s3=String::from("");
let s4=String::from(" ");
println!("s3 is_empty: {}, s4 is_empty: {}",s3.is_empty(),s4.is_empty()) // s3 is_empty: true, s4 is_empty: false

fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
    arg.as_ref().as_bytes().len()
}
fn char_counter<T: AsRef<str>>(arg: T) -> usize {
    arg.as_ref().chars().count()
}
let s = "Café au lait";
println!("chars len:{}, bytes len:{}",char_counter(s), byte_counter(s)); // chars len:12, bytes len:13
let s = "Cafe au lait";
println!("chars len:{}, bytes len:{}",char_counter(s), byte_counter(s)); // chars len:12, bytes len:12
let s = String::from("Café au lait");
println!("chars len:{}, bytes len:{}",char_counter(s.clone()), byte_counter(s)); // chars len:12, bytes len:13
let s = String::from("Cafe au lait");
println!("chars len:{}, bytes len:{}",char_counter(s.clone()), byte_counter(s)); // chars len:12, bytes len:12

子串搜索

let s = String::from("Hello,Rust");
println!("{}",s.contains("Rust")); // 是否包含子串,true
println!("{:?}",s.find("Rust")); // 返回Option类型,Some(6)

类型转换

let s = String::from("Hello,Rust");
println!("{}",s.as_str()); // String转&str,"Hello,Rust"
println!("{:?}",s.into_bytes()); // 转为字节数组,[72, 101, 108, 108, 111, 44, 82, 117, 115, 116]

转换

let s = String::from("Hello Rust");
println!("{}",s.replace(" ", ",")); // 替换,"Hello,Rust"
println!("{}",s.to_lowercase()); // 转小写,"hello rust"

let v:Vec<&str>=s.split_whitespace().collect(); // 按空格分割,返回一个迭代器
println!("{:?}",v); // ["Hello", "Rust"]
let v: Vec<&str> = s.split(' ').collect(); // 按指定字符分割,返回一个迭代器
println!("{:?}",v); // ["Hello", "Rust"]
let s = "hello\nworld\n";
let v: Vec<&str> = s.split_terminator('\n').collect(); // 包含终止符的分割
println!("{:?}",v); // ["hello", "world"]
let s = "1.2.3.4";
let v: Vec<&str> = s.rsplit('.').collect(); // 从右向左分割
println!("{:?}",v); // ["4", "3", "2", "1"] 
let s = "hello\tworld\rhello\nrust hi";
let v: Vec<&str> = s.split_ascii_whitespace().collect(); // 分割ASCII空白字符
println!("{:?}",v); // ["hello", "world", "hello", "rust", "hi"]
let s = "key=value";
if let Some((key, value)) = s.split_once('=') { // 只分割一次
    println!("{}: {}", key, value); // key: value
}

更新字符串

let mut s = String::from("Hello");
s.extend(" Rust".chars()); // 追加字符迭代器
println!("{}",s); // Hello Rust
let mut s = String::from("Hello");
s.extend([' ', 'R', 'u', 's', 't'].iter()); // 追加字符迭代器
println!("{}",s); // Hello Rust
let mut s = String::from("foo");
s.push(' '); // 使用 push 将一个字符加入 String 值中
s.push_str("bar"); // 使用 push_str 方法向 String 附加字符串 slice
println!("{}",s); // "foo bar"

使用 + 运算符拼接字符串

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用。字符串 s3 将会包含 `Hello, world!`。
// + 运算符使用了 add 函数,其签名:fn add(self, s: &str) -> String {},&s2 是 &String,被解引用强制转换成 &str
// 该语句会获取 s1 的所有权,附加上从 s2 中的内容,并返回结果的所有权。

使用 format! 宏拼接字符串

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3); // format!返回一个带有结果内容的 String,并且不会获取任何参数的所有权。

遍历字符串的方法:Rust 的字符串不支持索引,但可以使用 for 遍历获取字符串元素。有效的 Unicode 标量值可能会由不止一个字节组成。

let s1 = String::from("tic");
for c in s1.chars() {
    println!("{}", c); // 单独的 Unicode 标量值
}
for c in s1.bytes() {
    println!("{}", c); // 原始字节
}

HashMap

HashMap<K, V> 类型储存了一个键类型 K 对应一个值类型 V 的映射。它通过一个哈希函数来实现映射,决定如何将键和值放入内存中。HashMap 的所有的键必须是相同类型,值也必须都是相同类型。HashMap 的键可以是布尔型、整型、字符串,或任意实现了 Eq 和 Hash trait 的其他类型。和 vector 类似,HashMap 也是可增长的,但 HashMap 在占据了多余空间时还可以缩小自己。可以使用 HashMap::with_capacity(unit) 创建具有一定初始容量的 HashMap,也可以使用 HashMap::new() 来获得一个带有默认初始容量的 HashMap(这是推荐方式)。

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
println!("{:?}",scores); // 乱序打印 {"Blue": 10, "Yellow": 50}

let teams  = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
println!("{:?}",scores); // 乱序打印 {"Blue": 10, "Yellow": 50}

HashMap 和所有权:对于像 i32 这样的实现了 Copy trait 的类型,其值可以拷贝进 HashMap。对于像 String 这样拥有所有权的值,其值将被移动而 HashMap 会成为这些值的所有者。如果将值的引用插入 HashMap,这些值本身将不会被移动进 HashMap。但是这些引用指向的值必须至少在 HashMap 有效时也是有效的。

use std::collections::HashMap;

let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// 这里 field_name 和 field_value 不再有效,

访问 HashMap 中的值

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name); // 返回 Option<V>

// 遍历
for (key, value) in &scores {
    println!("{}: {}", key, value);
}

更新哈希 map

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25); // 覆盖旧值
println!("{:?}", scores); // {"Blue": 25}

scores.entry(String::from("Yellow")).or_insert(50); //  在键对应的值不存在则将参数作为新值插入并返回新值的可变引用
scores.entry(String::from("Blue")).or_insert(50); // 在键对应的值存在时就返回这个值的可变引用
println!("{:?}", scores); // 乱序打印 {"Blue": 25, "Yellow": 50}

根据旧值更新一个值

use std::collections::HashMap;

let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}
println!("{:?}", map); // 乱序打印 {"world": 2, "hello": 1, "wonderful": 1}

HashSet

HashSet 可以当成这样的 HashMap:我们只关心其中的键而非值(HashSet<T> 实际上只是对 HashMap<T, ()> 的封装)。HashSet 保证了不会出现重复的元素。这是任何 set 集合类型遵循的规定。HashSet 只是它的一个实现。

集合(set)拥有 4 种基本操作(下面的调用全部都返回一个迭代器):

  • union(并集):获得两个集合中的所有元素(不含重复值)。
  • difference(差集):获取属于第一个集合而不属于第二集合的所有元素。
  • intersection(交集):获取同时属于两个集合的所有元素。
  • symmetric_difference(对称差):获取所有只属于其中一个集合,而不同时属于 两个集合的所有元素。
use std::collections::HashSet;

fn main() {
    let mut a: HashSet<i32> = vec!(1i32, 2, 3).into_iter().collect();
    let mut b: HashSet<i32> = vec!(2i32, 3, 4).into_iter().collect();
    assert!(a.insert(4));
    assert!(a.contains(&4));
    //assert!(b.insert(4), "Value 4 is already in set B!");// 如果值已经存在,那么 `HashSet::insert()` 返回 false。
    b.insert(5);

    // 若一个集合(collection)的元素类型实现了 `Debug`,那么该集合也就实现了 `Debug`。
    println!("A: {:?}", a); // 乱序打印 A: {4, 1, 2, 3}
    println!("B: {:?}", b); // 乱序打印 B: {2, 3, 4, 5}

    // 乱序打印 [1, 2, 3, 4, 5]。
    println!("Union: {:?}", a.union(&b).collect::<Vec<&i32>>());

    // 这将会打印出 [1]
    println!("Difference: {:?}", a.difference(&b).collect::<Vec<&i32>>());

    // 乱序打印 [2, 3, 4]。
    println!("Intersection: {:?}", a.intersection(&b).collect::<Vec<&i32>>());

    // 打印 [1, 5]
    println!("Symmetric Difference: {:?}", a.symmetric_difference(&b).collect::<Vec<&i32>>());
}

路径和 IO

路径

Path 结构体代表了底层文件系统的文件路径。Path 分为两种:posix::Path(针对 类 UNIX 系统)、 windows::Path(针对 Windows)。prelude 会选择并输出符合平台类型的 Path 种类。注意 Path 在内部并不是用 UTF-8 字符串表示的,而是存储为若干字节(Vec<u8>)的 vector。因此,将 Path 转化成 &str 并非零开销的,且可能失败(因此它返回一个 Option)。

use std::path::Path;

fn main() {
    // 从 `&'static str` 创建一个 `Path`
    let path = Path::new(".");

    // `join` 使用操作系统特定的分隔符来合并路径到一个字节容器,并返回新的路径
    let new_path = path.join("a").join("b");

    // 将路径转换成一个字符串切片
    match new_path.to_str() {
        None => panic!("new path is not a valid UTF-8 sequence"),
        Some(s) => println!("new path is {}", s),
    }
}

输出:new path is ./a/b

文件输入输出(I/O)

File 结构体表示一个被打开的文件,它包裹了一个文件描述符,并赋予了对所表示的文件的读写能力。由于在进行文件 I/O 操作时可能出现各种错误,因此 File 的所有方法都返回 io::Result<T> 类型,它是 Result<T, io::Error> 的别名。这使得所有 I/O 操作的失败都变成显式的。

打开文件 File::open 静态方法能够以只读模式打开一个文件。File 拥有资源,即文件描述符,它会在自身被 drop 时关闭文件。

use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

fn main() {
    // 创建指向所需的文件的路径
    let path = Path::new("hello.txt");
    let display = path.display();

    // 以只读方式打开路径,返回 `io::Result<File>`
    let mut file = match File::open(&path) {
        // `io::Error` 的 `description` 方法返回一个描述错误的字符串。
        Err(why) => panic!("couldn't open {}: {}", display, why.description()),
        Ok(file) => file,
    };

    // 读取文件内容到一个字符串,返回 `io::Result<usize>`
    let mut s = String::new();
    match file.read_to_string(&mut s) {
        Err(why) => panic!("couldn't read {}: {}", display, why.description()),
        Ok(_) => print!("{} contains:\n{}", display, s),
    }
    // `file` 离开作用域,并且 `hello.txt` 文件将被关闭。
}

创建文件 File::create 静态方法以只写模式打开一个文件。若文件已经存在,则旧内容将被销毁。否则将创建一个新文件。

static LOREM_IPSUM: &'static str =
"hello
world
";

use std::error::Error;
use std::io::prelude::*;
use std::fs::File;
use std::path::Path;

fn main() {
    let path = Path::new("out/lorem_ipsum.txt");
    let display = path.display();

    // 以只写模式打开文件,返回 `io::Result<File>`
    let mut file = match File::create(&path) {
        Err(why) => panic!("couldn't create {}: {}", display, why.description()),
        Ok(file) => file,
    };

    // 将 `LOREM_IPSUM` 字符串写进 `file`,返回 `io::Result<()>`
    match file.write_all(LOREM_IPSUM.as_bytes()) {
        Err(why) => panic!("couldn't write to {}: {}", display, why.description()),
        Ok(_) => println!("successfully wrote to {}", display),
    }
}

读取行:File::open 需要一个泛型 AsRef<Path>。字符串字面量&str、String、PathBuf等类型实现了AsRef trait,因此可以直接传递给接收P: AsRef泛型参数的函数‌。编译器会自动调用as_ref()方法完成转换

use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;

fn main() {
    if let Ok(lines) = read_lines("./hosts") {
        // 使用迭代器,返回一个(可选)字符串
        for line in lines {
            if let Ok(ip) = line {
                println!("{}", ip);
            }
        }
    }
}

// 输出包裹在 Result 中以允许匹配错误,将迭代器返回给文件行的读取器(Reader)。
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

文件系统操作

use std::fs;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::prelude::*;
use std::os::unix;
use std::path::Path;

// `% cat path` 的简单实现
fn cat(path: &Path) -> io::Result<String> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

// `% echo s > path` 的简单实现
fn echo(s: &str, path: &Path) -> io::Result<()> {
    let mut f = File::create(path)?;

    f.write_all(s.as_bytes())
}

// `% touch path` 的简单实现(忽略已存在的文件)
fn touch(path: &Path) -> io::Result<()> {
    match OpenOptions::new().create(true).write(true).open(path) {
        Ok(_) => Ok(()),
        Err(e) => Err(e),
    }
}

fn main() {
    println!("`mkdir a`");
    // 创建一个目录,返回 `io::Result<()>`
    match fs::create_dir("a") {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(_) => {},
    }

    println!("`echo hello > a/b.txt`");
    // 前面的匹配可以用 `unwrap_or_else` 方法简化
    echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`mkdir -p a/c/d`");
    // 递归地创建一个目录,返回 `io::Result<()>`
    fs::create_dir_all("a/c/d").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`touch a/c/e.txt`");
    touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`ln -s ../b.txt a/c/b.txt`");
    // 创建一个符号链接,返回 `io::Resutl<()>`
    if cfg!(target_family = "unix") {
        unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
        });
    }

    println!("`cat a/c/b.txt`");
    match cat(&Path::new("a/c/b.txt")) {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(s) => println!("> {}", s),
    }

    println!("`ls a`");
    // 读取目录的内容,返回 `io::Result<Vec<Path>>`
    match fs::read_dir("a") {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(paths) => for path in paths {
            println!("> {:?}", path.unwrap().path());
        },
    }

    println!("`rm a/c/e.txt`");
    // 删除一个文件,返回 `io::Result<()>`
    fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`rmdir a/c/d`");
    // 移除一个空目录,返回 `io::Result<()>`
    fs::remove_dir("a/c/d").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });
}
posted @ 2025-07-31 10:31  carol2014  阅读(21)  评论(0)    收藏  举报