Android程序员初学Rust-标准库类型

59 阅读8分钟

1.jpg

开始之前,我们先来看下上一文的答案:

fn min<T:Ord>(a:T, b:T) -> T {
    if a < b {
        a
    } else {
        b
    }
}

非常简单,不是吗?

今天,我们讲 Rust 中的标准库类型。

Rust 附带了一个标准库,该标准库有助于确立一套 Rust 库和程序常用的类型。如此一来,任意两个库就能顺利协作,因为它们都使用相同的 String 类型。

事实上,Rust 包含几层标准库:coreallocstd

  • core 包含最基本的类型和函数,这些类型和函数不依赖于 libc、分配器,甚至不依赖于操作系统。
  • alloc 包含需要全局堆分配器的类型,如 VecBoxArc
  • 嵌入式 Rust 应用程序通常只使用 core,有时也使用 alloc

文档

2.jpg

Rust 有丰富详尽的文档。例如:

  • 有关循环的所有细节。
  • u8 这样的基本类型。
  • 像 Option 或 BinaryHeap 这样的标准库类型。

使用 rustup doc --std 或访问 std.rs 即可查看文档。

实际上,你也可以为自己的代码撰写文档:

/// Determine whether the first argument is divisible by the second argument.
///
/// If the second argument is zero, the result is false.
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
    if rhs == 0 {
        return false;
    }
    lhs % rhs == 0
}

内容将按 Markdown 格式处理。所有发布的 Rust 库包都会使用 rustdoc 工具在 docs.rs 上自动生成文档。按照习惯,要用这种模式为 API 中的所有公共项编写文档。

要在项目内部(例如在模块内部)为项目添加文档说明,可以使用 //!/*!.. */,这被称为 “内部文档注释”:

//! This module contains functionality relating to divisibility of integers.

Option

3.jpg

我们已经见识过 Option<T> 的一些用法。它要么存储一个 T 类型的值,要么不存储任何值。例如,String::find 方法会返回一个 Option<usize>

fn main() {
    let name = "Löwe 老虎 Léopard Gepardi";
    let mut position: Option<usize> = name.find('é');
    dbg!(position);
    assert_eq!(position.unwrap(), 14);
    position = name.find('Z');
    dbg!(position);
    assert_eq!(position.expect("Character not found"), 0);
}

Rust 没有 NULL,如果我们想表达一个东西可空,用 Option<T>

Option 类型的应用非常广泛,并不局限于标准库中。

unwrap 方法会返回 Option 中的值,如果没有值则会导致程序 panicexpect 方法类似,但需要传入一条错误信息。

你可以在遇到 None 时引发程序 panic,但不能 “不小心” 忘记检查 None 的情况(想象一下在 Kotlin 中,你的程序因为异常崩溃。表面上崩溃的是你的程序,实际是你的心情)。

在临时拼凑代码时,到处使用 unwrapexpect 是很常见的,但在生产代码中,通常会以更优雅的方式处理 None 情况。

Result

4.jpg

ResultOption 类似,但它用于表示操作的成功或失败,通过不同的枚举变体来区分。它是泛型类型:Result<T, E>,其中 T 用于 Ok 变体,而 E 出现在 Err 变体中。

use std::fs::File;
use std::io::Read;

fn main() {
    let file: Result<File, std::io::Error> = File::open("diary.txt");
    match file {
        Ok(mut file) => {
            let mut contents = String::new();
            if let Ok(bytes) = file.read_to_string(&mut contents) {
                println!("Dear diary: {contents} ({bytes} bytes)");
            } else {
                println!("Could not read file content");
            }
        }
        Err(err) => {
            println!("The diary could not be opened: {err}");
        }
    }
}

Option 类似,成功的值存储在 Result 内部,这就要求开发人员显式地提取它。这有助于进行错误检查。在错误永远不应该发生的情况下,可以调用 unwrap()expect(),这也表明了开发人员的意图。

建议阅读 Result 的文档,它包含很多实用的方法和函数,有助于进行函数式风格的编程。

Rust 中,Result 是用于实现错误处理的标准类型。

String

5.jpg

String 是一个可增长的 UTF-8 编码字符串:

fn main() {
    let mut s1 = String::new();
    s1.push_str("Hello");
    println!("s1: len = {}, capacity = {}", s1.len(), s1.capacity());

    let mut s2 = String::with_capacity(s1.len() + 1);
    s2.push_str(&s1);
    s2.push('!');
    println!("s2: len = {}, capacity = {}", s2.len(), s2.capacity());

    let s3 = String::from("🇨🇭");
    println!("s3: len = {}, number of chars = {}", s3.len(), s3.chars().count());
}

// Output
// s1: len = 5, capacity = 8
// s2: len = 6, capacity = 6
// s3: len = 8, number of chars = 2

String 实现了 Deref<Target = str>,这意味着你可以在 String 上调用所有 str 的方法。

String::new 会返回一个新的空字符串。如果你知道要向字符串中添加多少数据,可以使用 String::with_capacity

String::len 返回字符串的字节大小(即该字符串占用空间的大小,这与字符串长度意义是不同的)。

String::chars 返回一个遍历实际字符的迭代器。请注意,由于字符簇的存在,一个 char 可能与我们通常所认为的 “字符” 有所不同。

当开发者提到字符串时,他们可能指的是 &strString

当一个类型实现了 Deref<Target = T>,编译器允许你透明地调用 T 的方法。

  • 我们还没有讨论 Deref 特性,所以目前这主要是为了解释文档侧边栏的结构。
  • String 实现了 Deref<Target = str>,这使得它可以透明地访问 str 的方法。

String 实现为字节向量的包装器,许多在向量上支持的操作在 String 上也受支持,但有一些额外的保证。

访问 String 的不同方式:

  • 通过 s3.chars().nth(i).unwrap() 访问字符,其中 i 在边界内或越界。
  • 通过 s3[0..4] 访问子字符串,无论该子字符串是否处于字符边界。

许多类型可以使用 to_string 方法转换为字符串。所有实现了 Display 的类型都会自动实现该特性,所以任何可以格式化的内容都可以转换为字符串。

Vec

6.jpg

Vec 是标准的可调整大小的堆分配缓冲区(类似 Kotlin 中的 List):

fn main() {
    let mut v1 = Vec::new();
    v1.push(42);
    println!("v1: len = {}, capacity = {}", v1.len(), v1.capacity());

    let mut v2 = Vec::with_capacity(v1.len() + 1);
    v2.extend(v1.iter());
    v2.push(9999);
    println!("v2: len = {}, capacity = {}", v2.len(), v2.capacity());

    // 用于使用元素初始化向量的宏
    let mut v3 = vec![0, 0, 1, 2, 3, 4];

    // 只保留偶数元素
    v3.retain(|x| x % 2 == 0);
    println!("{v3:?}");

    // 删除连续的重复项
    v3.dedup();
    println!("{v3:?}");
}

// Output
// v1: len = 1, capacity = 4
// v2: len = 2, capacity = 2
// [0, 0, 2, 4]
// [0, 2, 4]

Vec 实现了 Deref<Target = [T]>,这意味着你可以在 Vec 上调用切片方法。

VecStringHashMap 一样,是一种集合类型。它包含的数据存储在堆上。这意味着在编译时不需要知道数据量的大小。它可以在运行时增长或收缩。

注意,Vec<T> 也是一种泛型类型,但你不必显式指定 T。与 Rust 的类型推断一贯做法一样,T 是在第一次调用 push 时确定的。

vec![...] 是一个宏,可替代 Vec::new() 使用,它支持向向量中添加初始元素(还记得 Kotlin 中的 listOf 吗?)。

要对向量进行索引,可以使用 [],但如果越界会导致程序崩溃。使用 get 方法会返回一个 Optionpop 函数会删除最后一个元素。

HashMap

7.jpg

具有防哈希拒绝服务(HashDoS)攻击保护的标准库 HashMap

use std::collections::HashMap;

fn main() {
    let mut page_counts = HashMap::new();
    page_counts.insert("Adventures of Huckleberry Finn", 207);
    page_counts.insert("Grimms' Fairy Tales", 751);
    page_counts.insert("Pride and Prejudice", 303);

    if !page_counts.contains_key("Les Misérables") {
        println!(
            "We know about {} books, but not Les Misérables.",
            page_counts.len()
        );
    }

    for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
        match page_counts.get(book) {
            Some(count) => println!("{book}: {count} pages"),
            None => println!("{book} is unknown."),
        }
    }

    // 使用 .entry() 方法在未找到值时插入一个值。
    for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
        let page_count: &mut i32 = page_counts.entry(book).or_insert(0);
        *page_count += 1;
    }

    dbg!(page_counts);
}

HashDos 是一种攻击方式,攻击者通过精心构造输入数据,利用哈希算法的碰撞特性(即不同输入产生相同哈希值的情况),导致 HashMap 存储的数据退化成链表,系统资源被大量消耗,从而无法正常处理合法请求。Rust 标准库的 HashMap 可以防止此种攻击。

HashMap 未在预导入模块中定义,需要引入作用域,即导入相关的包/库。

试试以下几行代码。第一行会检查某个书籍是否在哈希映射中,如果不存在,则返回一个替代值。第二行会在未找到该书籍时将替代值插入哈希映射中。

let pc1 = page_counts
    .get("Harry Potter and the Sorcerer's Stone")
    .unwrap_or(&336);
let pc2 = page_counts
    .entry("The Hunger Games")
    .or_insert(374);

vec! 不同,HashMap 没有标准的 hashmap! 宏。

不过,从 Rust 1.56 版本开始,HashMap 实现了 From<[(K, V); N]>,这使我们能够轻松地从字面值数组初始化哈希映射:

let page_counts = HashMap::from([
  ("Harry Potter and the Sorcerer's Stone".to_string(), 336),
  ("The Hunger Games".to_string(), 374),
]);

或者,HashMap 可以从任何生成键值对元组的迭代器中构建而来。

练习

本次练习中,你将把一个非常简单的数据结构泛型化。它使用 std::collections::HashMap 来跟踪哪些值已经被看到过,以及每个值出现的次数。

Counter 的初始版本是硬编码的,只能处理 u32 类型的值。将其泛型化,这样 Counter 就可以跟踪任何类型的值。

use std::collections::HashMap;

/// Counter 统计类型为 T 的每个值出现的次数。
struct Counter {
    values: HashMap<u32, u64>,
}

impl Counter {
    /// 创建 Counter
    fn new() -> Self {
        Counter {
            values: HashMap::new(),
        }
    }

    /// 统计给定值的出现次数
    fn count(&mut self, value: u32) {
        if self.values.contains_key(&value) {
            *self.values.get_mut(&value).unwrap() += 1;
        } else {
            self.values.insert(value, 1);
        }
    }

    /// 返回给定值出现的次数
    fn times_seen(&self, value: u32) -> u64 {
        self.values.get(&value).copied().unwrap_or_default()
    }
}

fn main() {
    let mut ctr = Counter::new();
    ctr.count(13);
    ctr.count(14);
    ctr.count(16);
    ctr.count(14);
    ctr.count(14);
    ctr.count(11);

    for i in 10..20 {
        println!("saw {} values equal to {}", ctr.times_seen(i), i);
    }

    let mut strctr = Counter::new();
    strctr.count("apple");
    strctr.count("orange");
    strctr.count("apple");
    println!("got {} apples", strctr.times_seen("apple"));
}